tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74# --- Main constants: 75 76NANO = 0.000000001 # SI-constant nano = 10^-9 77 78 79def NanoToFloat(units: str, nano: int) -> float: 80 """ 81 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 82 83 `NanoToFloat(units="2", nano=500000000) -> 2.5` 84 85 `NanoToFloat(units="0", nano=50000000) -> 0.05` 86 87 :param units: integer string or integer parameter that represents the integer part of number 88 :param nano: integer string or integer parameter that represents the fractional part of number 89 :return: float view of number 90 """ 91 return int(units) + int(nano) * NANO 92 93 94def FloatToNano(number: float) -> dict: 95 """ 96 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 97 98 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 99 100 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 101 102 :param number: float number 103 :return: nano-type view of number: `{"units": "string", "nano": integer}` 104 """ 105 splitByPoint = str(number).split(".") 106 frac = 0 107 108 if len(splitByPoint) > 1: 109 if len(splitByPoint[1]) <= 9: 110 frac = int("{}{}".format( 111 int(splitByPoint[1]), 112 "0" * (9 - len(splitByPoint[1])), 113 )) 114 115 if (number < 0) and (frac > 0): 116 frac = -frac 117 118 return {"units": str(int(number)), "nano": frac} 119 120 121def GetDatesAsString(start: str = None, end: str = None) -> tuple: 122 """ 123 Create tuple of date and time strings with timezone parsed from user-friendly date. 124 125 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 126 127 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 128 An error exception will occur if input date has incorrect format. 129 130 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 131 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 132 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 133 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 134 135 Also, you can use keywords for start if `end=None`: 136 `today` (from 00:00:00 to the end of current day), 137 `yesterday` (-1 day from 00:00:00 to 23:59:59), 138 `week` (-7 day from 00:00:00 to the end of current day), 139 `month` (-30 day from 00:00:00 to the end of current day), 140 `year` (-365 day from 00:00:00 to the end of current day), 141 142 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 143 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 144 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 145 """ 146 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 147 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 148 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 149 150 # time between start and the end of the current day: 151 if start is None or start.lower() == "today": 152 pass 153 154 # from start of the last day to the end of the last day: 155 elif start.lower() == "yesterday": 156 s -= timedelta(days=1) 157 e -= timedelta(days=1) 158 159 # week (-7 day from 00:00:00 to the end of the current day): 160 elif start.lower() == "week": 161 s -= timedelta(days=6) # +1 current day already taken into account 162 163 # month (-30 day from 00:00:00 to the end of current day): 164 elif start.lower() == "month": 165 s -= timedelta(days=29) # +1 current day already taken into account 166 167 # year (-365 day from 00:00:00 to the end of current day): 168 elif start.lower() == "year": 169 s -= timedelta(days=364) # +1 current day already taken into account 170 171 # -N days ago to the end of current day: 172 elif start.startswith('-') and start[1:].isdigit(): 173 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 174 175 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 176 else: 177 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 178 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 179 180 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 181 s = s.strftime(TKS_DATE_TIME_FORMAT) 182 e = e.strftime(TKS_DATE_TIME_FORMAT) 183 184 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 185 186 return s, e 187 188 189class TinkoffBrokerServer: 190 """ 191 This class implements methods to work with Tinkoff broker server. 192 193 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 194 195 About `token`: https://tinkoff.github.io/investAPI/token/ 196 """ 197 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 198 """ 199 Main class init. 200 201 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 202 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 203 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 204 :param useCache: use default cache file with raw data to use instead of `iList`. 205 True by default. Cache is auto-update if new day has come. 206 If you don't want to use cache and always updates raw data then set `useCache=False`. 207 :param defaultCache: path to default cache file. `dump.json` by default. 208 """ 209 if token is None or not token: 210 try: 211 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 212 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 213 214 except KeyError: 215 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 216 raise Exception("Token required") 217 218 else: 219 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 220 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 221 222 if accountId is None or not accountId: 223 try: 224 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 225 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 226 227 except KeyError: 228 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 229 230 else: 231 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 232 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 233 234 self.version = __version__ # duplicate here used TKSBrokerAPI main version 235 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 236 237 Latest version: https://pypi.org/project/tksbrokerapi/ 238 """ 239 240 self.aliases = TKS_TICKER_ALIASES 241 """Some aliases instead official tickers. 242 243 See also: `TKSEnums.TKS_TICKER_ALIASES` 244 """ 245 246 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 247 248 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 249 250 self.ticker = "" 251 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 252 253 See also: `SearchByTicker()`, `SearchInstruments()`. 254 """ 255 256 self.figi = "" 257 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 258 259 See also: `SearchByFIGI()`, `SearchInstruments()`. 260 """ 261 262 self.depth = 1 263 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 264 265 See also: `GetCurrentPrices()`. 266 """ 267 268 self.server = r"https://invest-public-api.tinkoff.ru/rest" 269 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 270 271 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 272 """ 273 274 uLogger.debug("Broker API server: {}".format(self.server)) 275 276 self.timeout = 15 277 """Server operations timeout in seconds. Default: `15`. 278 279 See also: `SendAPIRequest()`. 280 """ 281 282 self.headers = { 283 "Content-Type": "application/json", 284 "accept": "application/json", 285 "Authorization": "Bearer {}".format(self.token), 286 "x-app-name": "Tim55667757.TKSBrokerAPI", 287 } 288 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.body = None 294 """Request body which send to broker server. Default: `None`. 295 296 See also: `SendAPIRequest()`. 297 """ 298 299 self.moreDebug = False 300 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 301 302 self.historyFile = None 303 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 304 305 See also: `History()`. 306 """ 307 308 self.htmlHistoryFile = "index.html" 309 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 310 311 See also: `ShowHistoryChart()`. 312 """ 313 314 self.instrumentsFile = "instruments.md" 315 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 316 317 See also: `ShowInstrumentsInfo()`. 318 """ 319 320 self.searchResultsFile = "search-results.md" 321 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 322 323 See also: `SearchInstruments()`. 324 """ 325 326 self.pricesFile = "prices.md" 327 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 328 329 See also: `GetListOfPrices()`. 330 """ 331 332 self.infoFile = "info.md" 333 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 334 335 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 336 """ 337 338 self.bondsXLSXFile = "ext-bonds.xlsx" 339 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 340 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 341 342 See also: `ExtendBondsData()`. 343 """ 344 345 self.calendarFile = "calendar.md" 346 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 347 348 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 349 350 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 351 """ 352 353 self.overviewFile = "overview.md" 354 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 355 356 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 357 """ 358 359 self.overviewDigestFile = "overview-digest.md" 360 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 361 362 See also: `Overview()` with parameter `details="digest"`. 363 """ 364 365 self.overviewPositionsFile = "overview-positions.md" 366 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 367 368 See also: `Overview()` with parameter `details="positions"`. 369 """ 370 371 self.overviewOrdersFile = "overview-orders.md" 372 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 373 374 See also: `Overview()` with parameter `details="orders"`. 375 """ 376 377 self.overviewAnalyticsFile = "overview-analytics.md" 378 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 379 380 See also: `Overview()` with parameter `details="analytics"`. 381 """ 382 383 self.reportFile = "deals.md" 384 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 385 386 See also: `Deals()`. 387 """ 388 389 self.withdrawalLimitsFile = "limits.md" 390 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 391 392 See also: `OverviewLimits()` and `RequestLimits()`. 393 """ 394 395 self.userInfoFile = "user-info.md" 396 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 397 398 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 399 """ 400 401 self.userAccountsFile = "accounts.md" 402 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 403 404 See also: `OverviewAccounts()`, `RequestAccounts()`. 405 """ 406 407 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 408 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 409 410 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 411 412 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 413 """ 414 415 self.iList = None # init iList for raw instruments data 416 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 417 418 See also: `Listing()`, `DumpInstruments()`. 419 """ 420 421 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 422 if useCache: 423 if os.path.exists(self.iListDumpFile): 424 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 425 curTime = datetime.now(tzutc()) 426 427 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 428 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 429 430 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 431 432 else: 433 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 434 435 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 436 os.path.abspath(self.iListDumpFile), 437 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 438 )) 439 440 else: 441 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 442 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 443 444 else: 445 self.iList = self.Listing() # request new raw instruments data from broker server 446 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 447 448 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 449 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 450 451 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 452 """ 453 454 def _ParseJSON(self, rawData="{}") -> dict: 455 """ 456 Parse JSON from response string. 457 458 :param rawData: this is a string with JSON-formatted text. 459 :return: JSON (dictionary), parsed from server response string. 460 """ 461 responseJSON = json.loads(rawData) if rawData else {} 462 463 if self.moreDebug: 464 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 465 466 return responseJSON 467 468 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 469 """ 470 Send GET or POST request to broker server and receive JSON object. 471 472 self.header: must be defining with dictionary of headers. 473 self.body: if define then used as request body. None by default. 474 self.timeout: global request timeout, 15 seconds by default. 475 :param url: url with REST request. 476 :param reqType: send "GET" or "POST" request. "GET" by default. 477 :param retry: how many times retry after first request if an 5xx server errors occurred. 478 :param pause: sleep time in seconds between retries. 479 :return: response JSON (dictionary) from broker. 480 """ 481 if reqType not in ("GET", "POST"): 482 uLogger.error("You can define request type: 'GET' or 'POST'!") 483 raise Exception("Incorrect value") 484 485 if self.moreDebug: 486 uLogger.debug("Request parameters:") 487 uLogger.debug(" - REST API URL: {}".format(url)) 488 uLogger.debug(" - request type: {}".format(reqType)) 489 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 490 uLogger.debug(" - body:\n{}".format(self.body)) 491 492 # fast hack to avoid all operations with some tickers/FIGI 493 responseJSON = {} 494 oK = True 495 for item in self.exclude: 496 if item in url: 497 if self.moreDebug: 498 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 499 500 oK = False 501 break 502 503 if oK: 504 counter = 0 505 response = None 506 errMsg = "" 507 508 while not response and counter <= retry: 509 if reqType == "GET": 510 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 511 512 if reqType == "POST": 513 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 514 515 if self.moreDebug: 516 uLogger.debug("Response:") 517 uLogger.debug(" - status code: {}".format(response.status_code)) 518 uLogger.debug(" - reason: {}".format(response.reason)) 519 uLogger.debug(" - body length: {}".format(len(response.text))) 520 uLogger.debug(" - headers:\n{}".format(response.headers)) 521 522 # Server returns some headers: 523 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 524 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 525 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 526 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 527 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 528 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 529 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 530 sleep(rateLimitWait) 531 532 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 533 if 400 <= response.status_code < 500: 534 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 535 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 536 counter = retry + 1 537 538 if 500 <= response.status_code < 600: 539 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, {}".format(errMsg)) 541 counter += 1 542 543 if counter <= retry: 544 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 545 sleep(pause) 546 547 responseJSON = self._ParseJSON(rawData=response.text) 548 549 if errMsg: 550 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 551 uLogger.error(" - not oK, {}".format(errMsg)) 552 553 return responseJSON 554 555 def _IUpdater(self, iType: str) -> tuple: 556 """ 557 Request instrument by type from server. See available API methods for instruments: 558 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 559 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 560 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 561 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 562 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 563 564 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 565 :return: tuple with iType name and list of available instruments of current type for defined user token. 566 """ 567 result = [] 568 569 if iType in TKS_INSTRUMENTS: 570 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 571 572 # all instruments have the same body in API v2 requests: 573 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 574 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 575 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 576 577 return iType, result 578 579 def _IWrapper(self, kwargs): 580 """ 581 Wrapper runs instrument's update method `_IUpdater()`. 582 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 583 """ 584 return self._IUpdater(**kwargs) 585 586 def Listing(self) -> dict: 587 """ 588 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 589 590 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 591 """ 592 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 593 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 594 595 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 596 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 597 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 598 599 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 600 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 601 poolUpdater.close() 602 603 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 604 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 605 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 606 607 # calculate minimum price increment (step) for all instruments and set up instrument's type: 608 for iType in iList.keys(): 609 for ticker in iList[iType]: 610 iList[iType][ticker]["type"] = iType 611 612 if "minPriceIncrement" in iList[iType][ticker].keys(): 613 iList[iType][ticker]["step"] = NanoToFloat( 614 iList[iType][ticker]["minPriceIncrement"]["units"], 615 iList[iType][ticker]["minPriceIncrement"]["nano"], 616 ) 617 618 else: 619 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 620 621 return iList 622 623 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 624 """ 625 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 626 627 See also: `DumpInstruments()`, `Listing()`. 628 629 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 630 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 631 """ 632 if self.iListDumpFile is None or not self.iListDumpFile: 633 uLogger.error("Output name of dump file must be defined!") 634 raise Exception("Filename required") 635 636 if not self.iList or forceUpdate: 637 self.iList = self.Listing() 638 639 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 640 641 # Save as XLSX with separated sheets for every type of instruments: 642 with pd.ExcelWriter( 643 path=xlsxDumpFile, 644 date_format=TKS_DATE_FORMAT, 645 datetime_format=TKS_DATE_TIME_FORMAT, 646 mode="w", 647 ) as writer: 648 for iType in TKS_INSTRUMENTS: 649 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 650 df = df[sorted(df)] # sorted by column names 651 df = df.applymap( 652 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 653 na_action="ignore", 654 ) # converting numbers from nano-type to float in every cell 655 df.to_excel( 656 writer, 657 sheet_name=iType, 658 encoding="UTF-8", 659 freeze_panes=(1, 1), 660 ) # saving as XLSX-file with freeze first row and column as headers 661 662 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 663 664 def DumpInstruments(self, forceUpdate: bool = True) -> str: 665 """ 666 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 667 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 668 669 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 670 671 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 672 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 673 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 674 """ 675 if self.iListDumpFile is None or not self.iListDumpFile: 676 uLogger.error("Output name of dump file must be defined!") 677 raise Exception("Filename required") 678 679 if not self.iList or forceUpdate: 680 self.iList = self.Listing() 681 682 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 683 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 684 fH.write(jsonDump) 685 686 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 687 688 return jsonDump 689 690 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 691 """ 692 Show information about one instrument defined by json data and prints it in Markdown format. 693 694 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 695 696 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 697 :param show: if `True` then also printing information about instrument and its current price. 698 :return: multilines text in Markdown format with information about one instrument. 699 """ 700 splitLine = "| | |\n" 701 infoText = "" 702 703 if iJSON is not None and iJSON and isinstance(iJSON, dict): 704 info = [ 705 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 706 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 707 "| Parameters | Values |\n", 708 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 709 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 710 "| Full name: | {:<54} |\n".format(iJSON["name"]), 711 ] 712 713 if "sector" in iJSON.keys() and iJSON["sector"]: 714 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 715 716 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 717 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 718 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 719 ))) 720 721 info.extend([ 722 splitLine, 723 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 724 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 725 ]) 726 727 if "isin" in iJSON.keys() and iJSON["isin"]: 728 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 729 730 if "classCode" in iJSON.keys(): 731 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 732 733 info.extend([ 734 splitLine, 735 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 736 splitLine, 737 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 738 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 739 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 740 ]) 741 742 if iJSON["figi"]: 743 self.figi = iJSON["figi"] 744 iJSON = iJSON | self.RequestTradingStatus() 745 746 info.extend([ 747 splitLine, 748 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 749 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 750 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 751 ]) 752 753 info.append(splitLine) 754 755 if "type" in iJSON.keys() and iJSON["type"]: 756 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 757 758 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 759 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 760 761 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 762 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 763 764 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 765 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 766 767 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 768 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 769 770 if "focusType" in iJSON.keys() and iJSON["focusType"]: 771 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 772 773 if "assetType" in iJSON.keys() and iJSON["assetType"]: 774 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 775 776 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 777 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 778 779 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 780 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 781 782 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 783 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 784 785 if "currency" in iJSON.keys(): 786 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 787 788 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 789 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 790 791 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 792 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 793 794 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 795 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 796 797 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 798 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 799 800 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 801 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 802 803 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 804 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 805 806 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 807 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 808 809 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 810 info.append("| Perpetual bond: | Yes |\n") 811 812 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 813 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 814 815 iExt = None 816 if iJSON["type"] == "Bonds": 817 info.extend([ 818 splitLine, 819 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 820 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 821 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 822 iJSON["nominal"]["currency"], 823 )), 824 ]) 825 826 if "floatingCouponFlag" in iJSON.keys(): 827 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 828 829 if "amortizationFlag" in iJSON.keys(): 830 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 831 832 info.append(splitLine) 833 834 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 835 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 836 837 if iJSON["figi"]: 838 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 839 840 info.extend([ 841 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 842 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 843 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 844 ]) 845 846 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 847 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 848 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 849 iJSON["aciValue"]["currency"] 850 ))) 851 852 if "currentPrice" in iJSON.keys(): 853 info.append(splitLine) 854 855 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 856 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 857 858 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 859 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 860 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 861 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 862 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 863 864 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 865 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 866 867 info.extend([ 868 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 869 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 870 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 871 )), 872 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Changes between last deal price and last close | {:<54} |\n".format( 877 "{:.2f}%{}".format( 878 iJSON["currentPrice"]["changes"], 879 " ({}{:.2f} {})".format( 880 "+" if bondChangesDelta > 0 else "", 881 bondChangesDelta, 882 aciCurrency 883 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 884 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 885 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 886 currency 887 ), 888 ) 889 ), 890 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 891 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 892 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 893 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 894 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 895 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 896 )), 897 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 898 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 899 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 900 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 901 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 902 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 903 )), 904 ]) 905 906 if "lot" in iJSON.keys(): 907 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 908 909 if "step" in iJSON.keys() and iJSON["step"] != 0: 910 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 911 912 # Add bond payment calendar: 913 if iJSON["type"] == "Bonds": 914 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 915 info.extend(["\n", strCalendar]) 916 917 infoText += "".join(info) 918 919 if show: 920 uLogger.info("{}".format(infoText)) 921 922 else: 923 uLogger.debug("{}".format(infoText)) 924 925 if self.infoFile is not None: 926 with open(self.infoFile, "w", encoding="UTF-8") as fH: 927 fH.write(infoText) 928 929 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 930 931 return infoText 932 933 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 934 """ 935 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 936 937 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 938 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 939 :return: JSON formatted data with information about instrument. 940 """ 941 tickerJSON = {} 942 if self.moreDebug: 943 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 944 945 if not self.ticker: 946 uLogger.warning("self.ticker variable is not be empty!") 947 948 else: 949 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 950 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 951 raise Exception("Instrument not allowed") 952 953 if not self.iList: 954 self.iList = self.Listing() 955 956 if self.ticker in self.iList["Shares"].keys(): 957 tickerJSON = self.iList["Shares"][self.ticker] 958 if self.moreDebug: 959 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 960 961 elif self.ticker in self.iList["Currencies"].keys(): 962 tickerJSON = self.iList["Currencies"][self.ticker] 963 if self.moreDebug: 964 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 965 966 elif self.ticker in self.iList["Bonds"].keys(): 967 tickerJSON = self.iList["Bonds"][self.ticker] 968 if self.moreDebug: 969 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 970 971 elif self.ticker in self.iList["Etfs"].keys(): 972 tickerJSON = self.iList["Etfs"][self.ticker] 973 if self.moreDebug: 974 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 975 976 elif self.ticker in self.iList["Futures"].keys(): 977 tickerJSON = self.iList["Futures"][self.ticker] 978 if self.moreDebug: 979 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 980 981 if tickerJSON: 982 self.figi = tickerJSON["figi"] 983 984 if requestPrice: 985 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 986 987 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 988 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 989 990 else: 991 tickerJSON["currentPrice"]["changes"] = 0 992 993 if show: 994 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 995 996 else: 997 if show: 998 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 999 1000 return tickerJSON 1001 1002 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1003 """ 1004 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1005 1006 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1007 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1008 :return: JSON formatted data with information about instrument. 1009 """ 1010 figiJSON = {} 1011 if self.moreDebug: 1012 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1013 1014 if not self.figi: 1015 uLogger.warning("self.figi variable is not be empty!") 1016 1017 else: 1018 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1019 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1020 raise Exception("Instrument not allowed") 1021 1022 if not self.iList: 1023 self.iList = self.Listing() 1024 1025 for item in self.iList["Shares"].keys(): 1026 if self.figi == self.iList["Shares"][item]["figi"]: 1027 figiJSON = self.iList["Shares"][item] 1028 1029 if self.moreDebug: 1030 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1031 1032 break 1033 1034 if not figiJSON: 1035 for item in self.iList["Currencies"].keys(): 1036 if self.figi == self.iList["Currencies"][item]["figi"]: 1037 figiJSON = self.iList["Currencies"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1041 1042 break 1043 1044 if not figiJSON: 1045 for item in self.iList["Bonds"].keys(): 1046 if self.figi == self.iList["Bonds"][item]["figi"]: 1047 figiJSON = self.iList["Bonds"][item] 1048 1049 if self.moreDebug: 1050 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1051 1052 break 1053 1054 if not figiJSON: 1055 for item in self.iList["Etfs"].keys(): 1056 if self.figi == self.iList["Etfs"][item]["figi"]: 1057 figiJSON = self.iList["Etfs"][item] 1058 1059 if self.moreDebug: 1060 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1061 1062 break 1063 1064 if not figiJSON: 1065 for item in self.iList["Futures"].keys(): 1066 if self.figi == self.iList["Futures"][item]["figi"]: 1067 figiJSON = self.iList["Futures"][item] 1068 1069 if self.moreDebug: 1070 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1071 1072 break 1073 1074 if figiJSON: 1075 self.figi = figiJSON["figi"] 1076 self.ticker = figiJSON["ticker"] 1077 1078 if requestPrice: 1079 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1080 1081 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1082 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1083 1084 else: 1085 figiJSON["currentPrice"]["changes"] = 0 1086 1087 if show: 1088 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1089 1090 else: 1091 if show: 1092 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1093 1094 return figiJSON 1095 1096 def GetCurrentPrices(self, show: bool = True) -> dict: 1097 """ 1098 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1099 `{"buy": [{"price": 1243.8, "quantity": 193}, 1100 {"price": 1244.0, "quantity": 168}, 1101 {"price": 1244.8, "quantity": 5}, 1102 {"price": 1245.0, "quantity": 61}, 1103 {"price": 1245.4, "quantity": 60}], 1104 "sell": [{"price": 1243.6, "quantity": 8}, 1105 {"price": 1242.6, "quantity": 10}, 1106 {"price": 1242.4, "quantity": 18}, 1107 {"price": 1242.2, "quantity": 50}, 1108 {"price": 1242.0, "quantity": 113}], 1109 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1110 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1111 - sell: list of dicts with Buyers prices, 1112 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1113 - quantity: volume value by current price in lots, 1114 - limitUp: current trade session limit price, maximum, 1115 - limitDown: current trade session limit price, minimum, 1116 - lastPrice: last deal price of the instrument, 1117 - closePrice: previous trade session close price of the instrument. 1118 1119 See also: `SearchByTicker()` and `SearchByFIGI()`. 1120 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1121 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 :param show: if `True` then print DOM to log and console. 1124 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1125 If an error occurred then returns an empty record: 1126 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1127 """ 1128 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1129 1130 if self.depth < 1: 1131 uLogger.error("Depth of Market (DOM) must be >=1!") 1132 raise Exception("Incorrect value") 1133 1134 if not (self.ticker or self.figi): 1135 uLogger.error("self.ticker or self.figi variables must be defined!") 1136 raise Exception("Ticker or FIGI required") 1137 1138 if self.ticker and not self.figi: 1139 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1140 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1141 1142 if not self.ticker and self.figi: 1143 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1144 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1145 1146 if not self.figi: 1147 uLogger.error("FIGI is not defined!") 1148 raise Exception("Ticker or FIGI required") 1149 1150 else: 1151 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1152 1153 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1154 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1155 self.body = str({"figi": self.figi, "depth": self.depth}) 1156 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1157 1158 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1159 # list of dicts with sellers orders: 1160 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1161 1162 # list of dicts with buyers orders: 1163 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1164 1165 # max price of instrument at this time: 1166 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1167 1168 # min price of instrument at this time: 1169 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1170 1171 # last price of deal with instrument: 1172 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1173 1174 # last close price of instrument: 1175 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1176 1177 else: 1178 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1179 uLogger.debug("Server response: {}".format(pricesResponse)) 1180 1181 if show: 1182 if prices["buy"] or prices["sell"]: 1183 info = [ 1184 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1185 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1186 self.ticker, 1187 self.figi, 1188 self.depth, 1189 ), 1190 "-" * 60, "\n", 1191 " Orders of Buyers | Orders of Sellers\n", 1192 "-" * 60, "\n", 1193 " Sell prices (volumes) | Buy prices (volumes)\n", 1194 "-" * 60, "\n", 1195 ] 1196 1197 if not prices["buy"]: 1198 info.append(" | No orders!\n") 1199 sumBuy = 0 1200 1201 else: 1202 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1203 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1204 for item in maxMinSorted: 1205 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1206 1207 if not prices["sell"]: 1208 info.append("No orders! |\n") 1209 sumSell = 0 1210 1211 else: 1212 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1213 for item in prices["sell"]: 1214 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1215 1216 info.extend([ 1217 "-" * 60, "\n", 1218 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1219 "-" * 60, "\n", 1220 ]) 1221 1222 infoText = "".join(info) 1223 1224 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1225 1226 else: 1227 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1228 1229 return prices 1230 1231 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1232 """ 1233 This method get and show information about all available broker instruments for current user account. 1234 If `instrumentsFile` string is not empty then also save information to this file. 1235 1236 :param show: if `True` then print results to console, if `False` - print only to file. 1237 :return: multi-lines string with all available broker instruments 1238 """ 1239 if not self.iList: 1240 self.iList = self.Listing() 1241 1242 info = [ 1243 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1244 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1245 ] 1246 1247 # add instruments count by type: 1248 for iType in self.iList.keys(): 1249 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1250 1251 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1252 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1253 1254 # generating info tables with all instruments by type: 1255 for iType in self.iList.keys(): 1256 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1257 1258 for instrument in self.iList[iType].keys(): 1259 iName = self.iList[iType][instrument]["name"] # instrument's name 1260 if len(iName) > 57: 1261 iName = "{}...".format(iName[:54]) # right trim for a long string 1262 1263 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1264 self.iList[iType][instrument]["ticker"], 1265 iName, 1266 self.iList[iType][instrument]["figi"], 1267 self.iList[iType][instrument]["currency"], 1268 self.iList[iType][instrument]["lot"], 1269 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1270 )) 1271 1272 infoText = "".join(info) 1273 1274 if show: 1275 uLogger.info(infoText) 1276 1277 if self.instrumentsFile: 1278 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1279 fH.write(infoText) 1280 1281 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1282 1283 return infoText 1284 1285 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1286 """ 1287 This method search and show information about instruments by part of its ticker, FIGI or name. 1288 If `searchResultsFile` string is not empty then also save information to this file. 1289 1290 :param pattern: string with part of ticker, FIGI or instrument's name. 1291 :param show: if `True` then print results to console, if `False` - return list of result only. 1292 :return: list of dictionaries with all found instruments. 1293 """ 1294 if not self.iList: 1295 self.iList = self.Listing() 1296 1297 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1298 compiledPattern = re.compile(pattern, re.IGNORECASE) 1299 1300 for iType in self.iList: 1301 for instrument in self.iList[iType].values(): 1302 searchResult = compiledPattern.search(" ".join( 1303 [instrument["ticker"], instrument["figi"], instrument["name"]] 1304 )) 1305 1306 if searchResult: 1307 searchResults[iType][instrument["ticker"]] = instrument 1308 1309 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1310 info = [ 1311 "# Search results\n\n", 1312 "* **Search pattern:** [{}]\n".format(pattern), 1313 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1314 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1315 ] 1316 infoShort = info[:] 1317 1318 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1319 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1320 skippedLine = "| ... | ... | ... | ... |\n" 1321 1322 if resultsLen == 0: 1323 info.append("\nNo results\n") 1324 infoShort.append("\nNo results\n") 1325 uLogger.warning("No results. Try changing your search pattern.") 1326 1327 else: 1328 for iType in searchResults: 1329 iTypeValuesCount = len(searchResults[iType].values()) 1330 if iTypeValuesCount > 0: 1331 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1332 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1333 1334 for instrument in searchResults[iType].values(): 1335 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1336 instrument["type"], 1337 instrument["ticker"], 1338 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1339 instrument["figi"], 1340 )) 1341 1342 if iTypeValuesCount <= 5: 1343 infoShort.extend(info[-iTypeValuesCount:]) 1344 1345 else: 1346 infoShort.extend(info[-5:]) 1347 infoShort.append(skippedLine) 1348 1349 infoText = "".join(info) 1350 infoTextShort = "".join(infoShort) 1351 1352 if show: 1353 uLogger.info(infoTextShort) 1354 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1355 1356 if self.searchResultsFile: 1357 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1358 fH.write(infoText) 1359 1360 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1361 1362 return searchResults 1363 1364 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1365 """ 1366 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1367 1368 :param instruments: list of strings with tickers or FIGIs. 1369 :return: list with unique instrument FIGIs only. 1370 """ 1371 requestedInstruments = [] 1372 for iName in instruments: 1373 if iName not in self.aliases.keys(): 1374 if iName not in requestedInstruments: 1375 requestedInstruments.append(iName) 1376 1377 else: 1378 if iName not in requestedInstruments: 1379 if self.aliases[iName] not in requestedInstruments: 1380 requestedInstruments.append(self.aliases[iName]) 1381 1382 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1383 1384 onlyUniqueFIGIs = [] 1385 for iName in requestedInstruments: 1386 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1387 continue 1388 1389 self.ticker = iName 1390 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1391 1392 if not iData: 1393 self.ticker = "" 1394 self.figi = iName 1395 1396 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1397 1398 if not iData: 1399 self.figi = "" 1400 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1401 1402 if iData and iData["figi"] not in onlyUniqueFIGIs: 1403 onlyUniqueFIGIs.append(iData["figi"]) 1404 1405 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1406 1407 return onlyUniqueFIGIs 1408 1409 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1410 """ 1411 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1412 See limits: https://tinkoff.github.io/investAPI/limits/ 1413 If `pricesFile` string is not empty then also save information to this file. 1414 1415 :param instruments: list of strings with tickers or FIGIs. 1416 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1417 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1418 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1419 """ 1420 if instruments is None or not instruments: 1421 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1422 raise Exception("Ticker or FIGI required") 1423 1424 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1425 1426 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1427 1428 iList = [] # trying to get info and current prices about all unique instruments: 1429 for self.figi in onlyUniqueFIGIs: 1430 iData = self.SearchByFIGI(requestPrice=True) 1431 iList.append(iData) 1432 1433 self.ShowListOfPrices(iList, show) 1434 1435 return iList 1436 1437 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1438 """ 1439 Show table contains current prices of given instruments. 1440 1441 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1442 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1443 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1444 :return: multilines text in Markdown format as a table contains current prices. 1445 """ 1446 infoText = "" 1447 1448 if show or self.pricesFile: 1449 info = [ 1450 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1451 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1452 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1453 ] 1454 1455 for item in iList: 1456 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1457 item["ticker"], 1458 item["figi"], 1459 item["type"], 1460 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1461 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1462 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1463 "{} / {}".format( 1464 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1465 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1466 ), 1467 "{} / {}".format( 1468 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1469 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1470 ), 1471 item["currency"], 1472 )) 1473 1474 infoText = "".join(info) 1475 1476 if show: 1477 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1478 1479 if self.pricesFile: 1480 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1481 fH.write(infoText) 1482 1483 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1484 1485 return infoText 1486 1487 def RequestTradingStatus(self) -> dict: 1488 """ 1489 Requesting trading status for the instrument defined by `figi` variable. 1490 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1491 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1492 1493 :return: dictionary with trading status attributes. Response example: 1494 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1495 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1496 """ 1497 if self.figi is None or not self.figi: 1498 uLogger.error("Variable `figi` must be defined for using this method!") 1499 raise Exception("FIGI required") 1500 1501 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1502 1503 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1504 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1505 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1506 1507 if self.moreDebug: 1508 uLogger.debug("Records about current trading status successfully received") 1509 1510 return tradingStatus 1511 1512 def RequestPortfolio(self) -> dict: 1513 """ 1514 Requesting actual user's portfolio for current `accountId`. 1515 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1516 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1517 1518 :return: dictionary with user's portfolio. 1519 """ 1520 if self.accountId is None or not self.accountId: 1521 uLogger.error("Variable `accountId` must be defined for using this method!") 1522 raise Exception("Account ID required") 1523 1524 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1525 1526 self.body = str({"accountId": self.accountId}) 1527 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1528 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1529 1530 if self.moreDebug: 1531 uLogger.debug("Records about user's portfolio successfully received") 1532 1533 return rawPortfolio 1534 1535 def RequestPositions(self) -> dict: 1536 """ 1537 Requesting open positions by currencies and instruments for current `accountId`. 1538 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1539 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1540 1541 :return: dictionary with open positions by instruments. 1542 """ 1543 if self.accountId is None or not self.accountId: 1544 uLogger.error("Variable `accountId` must be defined for using this method!") 1545 raise Exception("Account ID required") 1546 1547 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1548 1549 self.body = str({"accountId": self.accountId}) 1550 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1551 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1552 1553 if self.moreDebug: 1554 uLogger.debug("Records about current open positions successfully received") 1555 1556 return rawPositions 1557 1558 def RequestPendingOrders(self) -> list: 1559 """ 1560 Requesting current actual pending orders for current `accountId`. 1561 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1562 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1563 1564 :return: list of dictionaries with pending orders. 1565 """ 1566 if self.accountId is None or not self.accountId: 1567 uLogger.error("Variable `accountId` must be defined for using this method!") 1568 raise Exception("Account ID required") 1569 1570 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1571 1572 self.body = str({"accountId": self.accountId}) 1573 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1574 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1575 1576 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1577 1578 return rawOrders 1579 1580 def RequestStopOrders(self) -> list: 1581 """ 1582 Requesting current actual stop orders for current `accountId`. 1583 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1584 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1585 1586 :return: list of dictionaries with stop orders. 1587 """ 1588 if self.accountId is None or not self.accountId: 1589 uLogger.error("Variable `accountId` must be defined for using this method!") 1590 raise Exception("Account ID required") 1591 1592 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1593 1594 self.body = str({"accountId": self.accountId}) 1595 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1596 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1597 1598 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1599 1600 return rawStopOrders 1601 1602 def Overview(self, show: bool = False, details: str = "full") -> dict: 1603 """ 1604 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1605 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1606 are defined then also save information to file. 1607 1608 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1609 many requests about the state of the portfolio, and then, based on the received data, a large number 1610 of calculation and statistics are collected. 1611 1612 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1613 :param details: how detailed should the information be? You should specify one of strings: 1614 `full` - shows full available information about portfolio status (by default), 1615 `positions` - shows only open positions, 1616 `digest` - show a short digest of the portfolio status, 1617 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1618 `orders` - shows only sections of open limits and stop orders. 1619 :return: dictionary with client's raw portfolio and some statistics. 1620 """ 1621 if self.accountId is None or not self.accountId: 1622 uLogger.error("Variable `accountId` must be defined for using this method!") 1623 raise Exception("Account ID required") 1624 1625 view = { 1626 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1627 "headers": {}, # list of dictionaries, response headers without "positions" section 1628 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1629 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1630 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1631 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1632 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1633 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1634 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1635 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1636 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1637 }, 1638 "stat": { # --- some statistics calculated using "raw" sections: 1639 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1640 "availableRUB": 0., # available rubles (without other currencies) 1641 "blockedRUB": 0., # blocked sum in Russian Rouble 1642 "totalChangesRUB": 0., # changes for all open trades in RUB 1643 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1644 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1645 "sharesCostRUB": 0., # costs of all shares in RUB 1646 "bondsCostRUB": 0., # costs of all bonds in RUB 1647 "etfsCostRUB": 0., # costs of all etfs in RUB 1648 "futuresCostRUB": 0., # costs of all futures in RUB 1649 "Currencies": [], # list of dictionaries of all currencies statistics 1650 "Shares": [], # list of dictionaries of all shares statistics 1651 "Bonds": [], # list of dictionaries of all bonds statistics 1652 "Etfs": [], # list of dictionaries of all etfs statistics 1653 "Futures": [], # list of dictionaries of all futures statistics 1654 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1655 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1656 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1657 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1658 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1659 }, 1660 "analytics": { # --- some analytics of portfolio: 1661 "distrByAssets": {}, # portfolio distribution by assets 1662 "distrByCompanies": {}, # portfolio distribution by companies 1663 "distrBySectors": {}, # portfolio distribution by sectors 1664 "distrByCurrencies": {}, # portfolio distribution by currencies 1665 "distrByCountries": {}, # portfolio distribution by countries 1666 } 1667 } 1668 1669 details = details.lower() 1670 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1671 if details not in availableDetails: 1672 details = "full" 1673 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1674 1675 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1676 1677 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1678 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1679 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1680 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1681 1682 # save response headers without "positions" section: 1683 for key in portfolioResponse.keys(): 1684 if key != "positions": 1685 view["raw"]["headers"][key] = portfolioResponse[key] 1686 1687 else: 1688 continue 1689 1690 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1691 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1692 for item in portfolioResponse["positions"]: 1693 if item["instrumentType"] == "currency": 1694 self.figi = item["figi"] 1695 curr = self.SearchByFIGI(requestPrice=False) 1696 1697 # current price of currency in RUB: 1698 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1699 "name": curr["name"], 1700 "currentPrice": NanoToFloat( 1701 item["currentPrice"]["units"], 1702 item["currentPrice"]["nano"] 1703 ), 1704 } 1705 1706 view["raw"]["Currencies"].append(item) 1707 1708 elif item["instrumentType"] == "share": 1709 view["raw"]["Shares"].append(item) 1710 1711 elif item["instrumentType"] == "bond": 1712 view["raw"]["Bonds"].append(item) 1713 1714 elif item["instrumentType"] == "etf": 1715 view["raw"]["Etfs"].append(item) 1716 1717 elif item["instrumentType"] == "futures": 1718 view["raw"]["Futures"].append(item) 1719 1720 else: 1721 continue 1722 1723 # how many volume of currencies (by ISO currency name) are blocked: 1724 for item in view["raw"]["positions"]["blocked"]: 1725 blocked = NanoToFloat(item["units"], item["nano"]) 1726 if blocked > 0: 1727 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1728 1729 # how many volume of instruments (by FIGI) are blocked: 1730 for item in view["raw"]["positions"]["securities"]: 1731 blocked = int(item["blocked"]) 1732 if blocked > 0: 1733 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1734 1735 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1736 1737 if "rub" in allBlocked.keys(): 1738 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1739 1740 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1741 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1742 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1743 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1744 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1745 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1746 view["stat"]["portfolioCostRUB"] = sum([ 1747 view["stat"]["allCurrenciesCostRUB"], 1748 view["stat"]["sharesCostRUB"], 1749 view["stat"]["bondsCostRUB"], 1750 view["stat"]["etfsCostRUB"], 1751 view["stat"]["futuresCostRUB"], 1752 ]) 1753 1754 # --- calculating some portfolio statistics: 1755 byComp = {} # distribution by companies 1756 bySect = {} # distribution by sectors 1757 byCurr = {} # distribution by currencies (include RUB) 1758 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1759 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1760 1761 for item in portfolioResponse["positions"]: 1762 self.figi = item["figi"] 1763 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1764 1765 if instrument: 1766 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1767 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1768 1769 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1770 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1771 1772 else: 1773 blocked = 0 1774 1775 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1776 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1777 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1778 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1779 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1780 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1781 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1782 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1783 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1784 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1785 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1786 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1787 1788 statData = { 1789 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1790 "ticker": instrument["ticker"], # ticker by FIGI 1791 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1792 "volume": volume, # available volume of instrument 1793 "lots": lots, # volume in lots of instrument 1794 "direction": direction, # direction of an instrument's position: short or long 1795 "blocked": blocked, # blocked volume of currency or instrument 1796 "currentPrice": curPrice, # current instrument's price in basic asset 1797 "average": average, # current average position price 1798 "cost": cost, # current cost of all volume of instrument in basic asset 1799 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1800 "costRUB": costRUB, # cost of instrument in ruble 1801 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1802 "profit": profit, # expected profit at current moment 1803 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1804 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1805 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1806 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1807 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1808 "step": instrument["step"], # minimum price increment 1809 } 1810 1811 # adding distribution by unique countries: 1812 if statData["country"] not in byCountry.keys(): 1813 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1814 1815 else: 1816 byCountry[statData["country"]]["cost"] += costRUB 1817 byCountry[statData["country"]]["percent"] += percentCostRUB 1818 1819 if item["instrumentType"] != "currency": 1820 # adding distribution by unique companies: 1821 if statData["name"]: 1822 if statData["name"] not in byComp.keys(): 1823 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1824 1825 else: 1826 byComp[statData["name"]]["cost"] += costRUB 1827 byComp[statData["name"]]["percent"] += percentCostRUB 1828 1829 # adding distribution by unique sectors: 1830 if statData["sector"] not in bySect.keys(): 1831 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1832 1833 else: 1834 bySect[statData["sector"]]["cost"] += costRUB 1835 bySect[statData["sector"]]["percent"] += percentCostRUB 1836 1837 # adding distribution by unique currencies: 1838 if currency not in byCurr.keys(): 1839 byCurr[currency] = { 1840 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1841 "cost": costRUB, 1842 "percent": percentCostRUB 1843 } 1844 1845 else: 1846 byCurr[currency]["cost"] += costRUB 1847 byCurr[currency]["percent"] += percentCostRUB 1848 1849 # saving statistics for every instrument: 1850 if item["instrumentType"] == "currency": 1851 view["stat"]["Currencies"].append(statData) 1852 1853 # update dict with free funds for trading (total - blocked) by currencies 1854 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1855 view["stat"]["funds"][currency] = { 1856 "total": volume, 1857 "totalCostRUB": costRUB, # total volume cost in rubles 1858 "free": volume - blocked, 1859 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1860 } 1861 1862 elif item["instrumentType"] == "share": 1863 view["stat"]["Shares"].append(statData) 1864 1865 elif item["instrumentType"] == "bond": 1866 view["stat"]["Bonds"].append(statData) 1867 1868 elif item["instrumentType"] == "etf": 1869 view["stat"]["Etfs"].append(statData) 1870 1871 elif item["instrumentType"] == "Futures": 1872 view["stat"]["Futures"].append(statData) 1873 1874 else: 1875 continue 1876 1877 # total changes in Russian Ruble: 1878 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1879 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1880 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1881 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1882 view["stat"]["funds"]["rub"] = { 1883 "total": view["stat"]["availableRUB"], 1884 "totalCostRUB": view["stat"]["availableRUB"], 1885 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1886 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1887 } 1888 1889 # --- pending orders sector data: 1890 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1891 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1892 1893 for item in view["raw"]["orders"]: 1894 self.figi = item["figi"] 1895 1896 if item["figi"] not in uniquePendingOrdersFIGIs: 1897 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1898 1899 uniquePendingOrdersFIGIs.append(item["figi"]) 1900 uniquePendingOrders[item["figi"]] = instrument 1901 1902 else: 1903 instrument = uniquePendingOrders[item["figi"]] 1904 1905 if instrument: 1906 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1907 orderType = TKS_ORDER_TYPES[item["orderType"]] 1908 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1909 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1910 1911 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1912 if item["direction"] == "ORDER_DIRECTION_BUY": 1913 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1914 1915 else: 1916 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1917 1918 # requested price for order execution: 1919 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1920 1921 # necessary changes in percent to reach target from current price: 1922 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1923 1924 view["stat"]["orders"].append({ 1925 "orderID": item["orderId"], # orderId number parameter of current order 1926 "figi": item["figi"], # FIGI identification 1927 "ticker": instrument["ticker"], # ticker name by FIGI 1928 "lotsRequested": item["lotsRequested"], # requested lots value 1929 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1930 "currentPrice": lastPrice, # current instrument's price for defined action 1931 "targetPrice": target, # requested price for order execution in base currency 1932 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1933 "percentChanges": changes, # changes in percent to target from current price 1934 "currency": item["currency"], # instrument's currency name 1935 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1936 "type": orderType, # type of order from TKS_ORDER_TYPES 1937 "status": orderState, # order status from TKS_ORDER_STATES 1938 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1939 }) 1940 1941 # --- stop orders sector data: 1942 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1943 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1944 1945 for item in view["raw"]["stopOrders"]: 1946 self.figi = item["figi"] 1947 1948 if item["figi"] not in uniqueStopOrdersFIGIs: 1949 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1950 1951 uniqueStopOrdersFIGIs.append(item["figi"]) 1952 uniqueStopOrders[item["figi"]] = instrument 1953 1954 else: 1955 instrument = uniqueStopOrders[item["figi"]] 1956 1957 if instrument: 1958 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1959 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1960 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1961 1962 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1963 if "expirationTime" in item.keys(): 1964 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1965 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1966 1967 else: 1968 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1969 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1970 1971 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1972 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1973 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1974 1975 else: 1976 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1977 1978 # requested price when stop-order executed: 1979 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1980 1981 # price for limit-order, set up when stop-order executed: 1982 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1983 1984 # necessary changes in percent to reach target from current price: 1985 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1986 1987 view["stat"]["stopOrders"].append({ 1988 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1989 "figi": item["figi"], # FIGI identification 1990 "ticker": instrument["ticker"], # ticker name by FIGI 1991 "lotsRequested": item["lotsRequested"], # requested lots value 1992 "currentPrice": lastPrice, # current instrument's price for defined action 1993 "targetPrice": target, # requested price for stop-order execution in base currency 1994 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1995 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1996 "percentChanges": changes, # changes in percent to target from current price 1997 "currency": item["currency"], # instrument's currency name 1998 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1999 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2000 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2001 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2002 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2003 }) 2004 2005 # --- calculating data for analytics section: 2006 # portfolio distribution by assets: 2007 view["analytics"]["distrByAssets"] = { 2008 "Ruble": { 2009 "uniques": 1, 2010 "cost": view["stat"]["availableRUB"], 2011 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2012 }, 2013 "Currencies": { 2014 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2015 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2016 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2017 }, 2018 "Shares": { 2019 "uniques": len(view["stat"]["Shares"]), 2020 "cost": view["stat"]["sharesCostRUB"], 2021 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2022 }, 2023 "Bonds": { 2024 "uniques": len(view["stat"]["Bonds"]), 2025 "cost": view["stat"]["bondsCostRUB"], 2026 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2027 }, 2028 "Etfs": { 2029 "uniques": len(view["stat"]["Etfs"]), 2030 "cost": view["stat"]["etfsCostRUB"], 2031 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2032 }, 2033 "Futures": { 2034 "uniques": len(view["stat"]["Futures"]), 2035 "cost": view["stat"]["futuresCostRUB"], 2036 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2037 }, 2038 } 2039 2040 # portfolio distribution by companies: 2041 view["analytics"]["distrByCompanies"]["All money cash"] = { 2042 "ticker": "", 2043 "cost": view["stat"]["allCurrenciesCostRUB"], 2044 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2045 } 2046 view["analytics"]["distrByCompanies"].update(byComp) 2047 2048 # portfolio distribution by sectors: 2049 view["analytics"]["distrBySectors"]["All money cash"] = { 2050 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2051 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2052 } 2053 view["analytics"]["distrBySectors"].update(bySect) 2054 2055 # portfolio distribution by currencies: 2056 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2057 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2058 2059 if self.moreDebug: 2060 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2061 2062 view["analytics"]["distrByCurrencies"].update(byCurr) 2063 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2064 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2065 2066 # portfolio distribution by countries: 2067 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2068 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2069 2070 if self.moreDebug: 2071 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2072 2073 view["analytics"]["distrByCountries"].update(byCountry) 2074 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2075 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2076 2077 # --- Prepare text statistics overview in human-readable: 2078 if show: 2079 # Whatever the value `details`, header not changes: 2080 info = [ 2081 "# Client's portfolio\n\n", 2082 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2083 "* **Account ID:** [{}]\n".format(self.accountId), 2084 ] 2085 2086 if details in ["full", "positions", "digest"]: 2087 info.extend([ 2088 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2089 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2090 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2091 view["stat"]["totalChangesRUB"], 2092 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2093 view["stat"]["totalChangesPercentRUB"], 2094 ), 2095 ]) 2096 2097 if details in ["full", "positions"]: 2098 info.extend([ 2099 "## Open positions\n\n", 2100 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2101 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2102 "| Ruble | {:>31} | | | | | |\n".format( 2103 "{:.2f} ({:.2f}) rub".format( 2104 view["stat"]["availableRUB"], 2105 view["stat"]["blockedRUB"], 2106 ) 2107 ) 2108 ]) 2109 2110 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2111 return [ 2112 "| | | | | | | |\n", 2113 "| {:<27} | | | | | {:>19} | |\n".format( 2114 noTradeStr if noTradeStr else typeStr, 2115 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2116 ), 2117 ] 2118 2119 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2120 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2121 "{} [{}]".format(data["ticker"], data["figi"]), 2122 "{:.2f} ({:.2f}) {}".format( 2123 data["volume"], 2124 data["blocked"], 2125 data["currency"], 2126 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2127 data["volume"], 2128 data["blocked"], 2129 ), 2130 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2131 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2132 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2133 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2134 "{}{:.2f} {} ({}{:.2f}%)".format( 2135 "+" if data["profit"] > 0 else "", 2136 data["profit"], data["baseCurrencyName"], 2137 "+" if data["percentProfit"] > 0 else "", 2138 data["percentProfit"], 2139 ), 2140 ) 2141 2142 # --- Show currencies section: 2143 if view["stat"]["Currencies"]: 2144 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2145 for item in view["stat"]["Currencies"]: 2146 info.append(_InfoStr(item, showCurrencyName=True)) 2147 2148 else: 2149 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2150 2151 # --- Show shares section: 2152 if view["stat"]["Shares"]: 2153 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2154 2155 for item in view["stat"]["Shares"]: 2156 info.append(_InfoStr(item)) 2157 2158 else: 2159 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2160 2161 # --- Show bonds section: 2162 if view["stat"]["Bonds"]: 2163 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2164 2165 for item in view["stat"]["Bonds"]: 2166 info.append(_InfoStr(item)) 2167 2168 else: 2169 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2170 2171 # --- Show etfs section: 2172 if view["stat"]["Etfs"]: 2173 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2174 2175 for item in view["stat"]["Etfs"]: 2176 info.append(_InfoStr(item)) 2177 2178 else: 2179 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2180 2181 # --- Show futures section: 2182 if view["stat"]["Futures"]: 2183 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2184 2185 for item in view["stat"]["Futures"]: 2186 info.append(_InfoStr(item)) 2187 2188 else: 2189 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2190 2191 if details in ["full", "orders"]: 2192 # --- Show pending orders section: 2193 if view["stat"]["orders"]: 2194 info.extend([ 2195 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2196 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2197 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2198 ]) 2199 2200 for item in view["stat"]["orders"]: 2201 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2202 "{} [{}]".format(item["ticker"], item["figi"]), 2203 item["orderID"], 2204 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2205 "{} {} ({}{:.2f}%)".format( 2206 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2207 item["baseCurrencyName"], 2208 "+" if item["percentChanges"] > 0 else "", 2209 float(item["percentChanges"]), 2210 ), 2211 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2212 item["action"], 2213 item["type"], 2214 item["date"], 2215 )) 2216 2217 else: 2218 info.append("\n## Total pending limit-orders: 0\n") 2219 2220 # --- Show stop orders section: 2221 if view["stat"]["stopOrders"]: 2222 info.extend([ 2223 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2224 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2225 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2226 ]) 2227 2228 for item in view["stat"]["stopOrders"]: 2229 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2230 "{} [{}]".format(item["ticker"], item["figi"]), 2231 item["orderID"], 2232 item["lotsRequested"], 2233 "{} {} ({}{:.2f}%)".format( 2234 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2235 item["baseCurrencyName"], 2236 "+" if item["percentChanges"] > 0 else "", 2237 float(item["percentChanges"]), 2238 ), 2239 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2240 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2241 item["action"], 2242 item["type"], 2243 item["expType"], 2244 item["createDate"], 2245 item["expDate"], 2246 )) 2247 2248 else: 2249 info.append("\n## Total stop-orders: 0\n") 2250 2251 if details in ["full", "analytics"]: 2252 # -- Show analytics section: 2253 if view["stat"]["portfolioCostRUB"] > 0: 2254 info.extend([ 2255 "\n# Analytics\n" 2256 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2257 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2258 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2259 view["stat"]["totalChangesRUB"], 2260 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2261 view["stat"]["totalChangesPercentRUB"], 2262 ), 2263 "\n## Portfolio distribution by assets\n" 2264 "\n| Type | Uniques | Percent | Current cost |\n", 2265 "|------------|---------|---------|--------------------|\n", 2266 ]) 2267 2268 for key in view["analytics"]["distrByAssets"].keys(): 2269 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2270 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2271 key, 2272 view["analytics"]["distrByAssets"][key]["uniques"], 2273 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2274 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2275 )) 2276 2277 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2278 info.extend([ 2279 "\n## Portfolio distribution by companies\n" 2280 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2281 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2282 ]) 2283 2284 for company in view["analytics"]["distrByCompanies"].keys(): 2285 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2286 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2287 info.append("| {} | {:<7} | {:<18} |\n".format( 2288 "{}{}{}".format( 2289 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2290 company, 2291 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2292 ), 2293 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2294 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2295 )) 2296 2297 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2298 info.extend([ 2299 "\n## Portfolio distribution by sectors\n" 2300 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2301 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2302 ]) 2303 2304 for sector in view["analytics"]["distrBySectors"].keys(): 2305 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2306 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2307 sector, 2308 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2309 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2310 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2311 )) 2312 2313 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2314 info.extend([ 2315 "\n## Portfolio distribution by currencies\n" 2316 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2317 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2318 ]) 2319 2320 for curr in view["analytics"]["distrByCurrencies"].keys(): 2321 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2322 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2323 info.append("| {} | {:<7} | {:<18} |\n".format( 2324 "[{}] {}{}".format( 2325 curr, 2326 view["analytics"]["distrByCurrencies"][curr]["name"], 2327 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2328 ), 2329 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2330 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2331 )) 2332 2333 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2334 info.extend([ 2335 "\n## Portfolio distribution by countries\n" 2336 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2337 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2338 ]) 2339 2340 for country in view["analytics"]["distrByCountries"].keys(): 2341 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2342 nameLen = len(country) 2343 info.append("| {} | {:<7} | {:<18} |\n".format( 2344 "{}{}".format( 2345 country, 2346 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2347 ), 2348 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2349 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2350 )) 2351 2352 infoText = "".join(info) 2353 2354 uLogger.info(infoText) 2355 2356 if details == "full" and self.overviewFile: 2357 filename = self.overviewFile 2358 2359 elif details == "digest" and self.overviewDigestFile: 2360 filename = self.overviewDigestFile 2361 2362 elif details == "positions" and self.overviewPositionsFile: 2363 filename = self.overviewPositionsFile 2364 2365 elif details == "orders" and self.overviewOrdersFile: 2366 filename = self.overviewOrdersFile 2367 2368 elif details == "analytics" and self.overviewAnalyticsFile: 2369 filename = self.overviewAnalyticsFile 2370 2371 else: 2372 filename = "" 2373 2374 if filename: 2375 with open(filename, "w", encoding="UTF-8") as fH: 2376 fH.write(infoText) 2377 2378 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2379 2380 return view 2381 2382 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2383 """ 2384 Returns history operations between two given dates for current `accountId`. 2385 If `reportFile` string is not empty then also save human-readable report. 2386 Shows some statistical data of closed positions. 2387 2388 :param start: see docstring in `GetDatesAsString()` method 2389 :param end: see docstring in `GetDatesAsString()` method 2390 :param show: if `True` then also prints all records to the console. 2391 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2392 :return: original list of dictionaries with history of deals records from API ("operations" key): 2393 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2394 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2395 """ 2396 if self.accountId is None or not self.accountId: 2397 uLogger.error("Variable `accountId` must be defined for using this method!") 2398 raise Exception("Account ID required") 2399 2400 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2401 2402 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2403 2404 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2405 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2406 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2407 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2408 customStat = {} # custom statistics in additional to responseJSON 2409 2410 # --- output report in human-readable format: 2411 if show or self.reportFile: 2412 splitLine1 = "| | | | | |\n" # Summary section 2413 splitLine2 = "| | | | | | | | |\n" # Operations section 2414 nextDay = "" 2415 2416 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2417 2418 if len(ops) > 0: 2419 customStat = { 2420 "opsCount": 0, # total operations count 2421 "buyCount": 0, # buy operations 2422 "sellCount": 0, # sell operations 2423 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2424 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2425 "payIn": {"rub": 0.}, # Deposit brokerage account 2426 "payOut": {"rub": 0.}, # Withdrawals 2427 "divs": {"rub": 0.}, # Dividends income 2428 "coupons": {"rub": 0.}, # Coupon's income 2429 "brokerCom": {"rub": 0.}, # Service commissions 2430 "serviceCom": {"rub": 0.}, # Service commissions 2431 "marginCom": {"rub": 0.}, # Margin commissions 2432 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2433 } 2434 2435 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2436 for item in ops: 2437 if item["state"] == "OPERATION_STATE_EXECUTED": 2438 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2439 2440 # count buy operations: 2441 if "_BUY" in item["operationType"]: 2442 customStat["buyCount"] += 1 2443 2444 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2445 customStat["buyTotal"][item["payment"]["currency"]] += payment 2446 2447 else: 2448 customStat["buyTotal"][item["payment"]["currency"]] = payment 2449 2450 # count sell operations: 2451 elif "_SELL" in item["operationType"]: 2452 customStat["sellCount"] += 1 2453 2454 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2455 customStat["sellTotal"][item["payment"]["currency"]] += payment 2456 2457 else: 2458 customStat["sellTotal"][item["payment"]["currency"]] = payment 2459 2460 # count incoming operations: 2461 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2462 if item["payment"]["currency"] in customStat["payIn"].keys(): 2463 customStat["payIn"][item["payment"]["currency"]] += payment 2464 2465 else: 2466 customStat["payIn"][item["payment"]["currency"]] = payment 2467 2468 # count withdrawals operations: 2469 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2470 if item["payment"]["currency"] in customStat["payOut"].keys(): 2471 customStat["payOut"][item["payment"]["currency"]] += payment 2472 2473 else: 2474 customStat["payOut"][item["payment"]["currency"]] = payment 2475 2476 # count dividends income: 2477 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2478 if item["payment"]["currency"] in customStat["divs"].keys(): 2479 customStat["divs"][item["payment"]["currency"]] += payment 2480 2481 else: 2482 customStat["divs"][item["payment"]["currency"]] = payment 2483 2484 # count coupon's income: 2485 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2486 if item["payment"]["currency"] in customStat["coupons"].keys(): 2487 customStat["coupons"][item["payment"]["currency"]] += payment 2488 2489 else: 2490 customStat["coupons"][item["payment"]["currency"]] = payment 2491 2492 # count broker commissions: 2493 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2494 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2495 customStat["brokerCom"][item["payment"]["currency"]] += payment 2496 2497 else: 2498 customStat["brokerCom"][item["payment"]["currency"]] = payment 2499 2500 # count service commissions: 2501 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2502 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2503 customStat["serviceCom"][item["payment"]["currency"]] += payment 2504 2505 else: 2506 customStat["serviceCom"][item["payment"]["currency"]] = payment 2507 2508 # count margin commissions: 2509 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2510 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2511 customStat["marginCom"][item["payment"]["currency"]] += payment 2512 2513 else: 2514 customStat["marginCom"][item["payment"]["currency"]] = payment 2515 2516 # count withholding taxes: 2517 elif "_TAX" in item["operationType"]: 2518 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2519 customStat["allTaxes"][item["payment"]["currency"]] += payment 2520 2521 else: 2522 customStat["allTaxes"][item["payment"]["currency"]] = payment 2523 2524 else: 2525 continue 2526 2527 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2528 2529 # --- view "Actions" lines: 2530 info.extend([ 2531 "| Report sections | | | | |\n", 2532 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2533 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2534 "| | Buy: {:<22} | {:<28} | | |\n".format( 2535 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2536 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2537 ), 2538 "| | Sell: {:<21} | {:<28} | | |\n".format( 2539 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2540 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2541 ), 2542 ]) 2543 2544 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2545 for key in opsKeys: 2546 if key == "rub": 2547 continue 2548 2549 info.extend([ 2550 "| | | {:<28} | | |\n".format( 2551 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2552 ), 2553 "| | | {:<28} | | |\n".format( 2554 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2555 ), 2556 ]) 2557 2558 info.append(splitLine1) 2559 2560 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2561 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2562 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2563 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2564 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2565 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2566 ) 2567 2568 # --- view "Payments" lines: 2569 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2570 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2571 2572 for key in paymentsKeys: 2573 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2574 2575 info.append(splitLine1) 2576 2577 # --- view "Commissions and taxes" lines: 2578 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2579 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2580 2581 for key in comKeys: 2582 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2583 2584 info.append(splitLine1) 2585 2586 info.extend([ 2587 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2588 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2589 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2590 ]) 2591 2592 else: 2593 info.append("Broker returned no operations during this period\n") 2594 2595 # --- view "Operations" section: 2596 for item in ops: 2597 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2598 continue 2599 2600 else: 2601 self.figi = item["figi"] if item["figi"] else "" 2602 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2603 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2604 2605 # group of deals during one day: 2606 if nextDay and item["date"].split("T")[0] != nextDay: 2607 info.append(splitLine2) 2608 nextDay = "" 2609 2610 else: 2611 nextDay = item["date"].split("T")[0] # saving current day for splitting 2612 2613 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2614 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2615 self.figi if self.figi else "—", 2616 instrument["ticker"] if instrument else "—", 2617 instrument["type"] if instrument else "—", 2618 item["quantity"] if int(item["quantity"]) > 0 else "—", 2619 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2620 TKS_OPERATION_STATES[item["state"]], 2621 TKS_OPERATION_TYPES[item["operationType"]], 2622 )) 2623 2624 infoText = "".join(info) 2625 2626 if show: 2627 if self.moreDebug: 2628 uLogger.debug("Records about history of a client's operations successfully received") 2629 2630 uLogger.info(infoText) 2631 2632 if self.reportFile: 2633 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2634 fH.write(infoText) 2635 2636 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2637 2638 return ops, customStat 2639 2640 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2641 """ 2642 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2643 2644 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2645 Warning! Broker server used ISO UTC time by default. 2646 2647 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2648 Also, `historyFile` used to update history with `onlyMissing` parameter. 2649 2650 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2651 2652 :param start: see docstring in `GetDatesAsString()` method. 2653 :param end: see docstring in `GetDatesAsString()` method. 2654 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2655 `"hour"`, `"day"`. Default: `"hour"`. 2656 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2657 False by default. Warning! History appends only from last candle to current time 2658 with always update last candle! 2659 :param csvSep: separator if csv-file is used, `,` by default. 2660 :param show: if `True` then also prints Pandas DataFrame to the console. 2661 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2662 `["date", "time", "open", "high", "low", "close", "volume"]`. 2663 """ 2664 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2665 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2666 history = None # empty pandas object for history 2667 2668 if interval not in TKS_CANDLE_INTERVALS.keys(): 2669 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2670 raise Exception("Incorrect value") 2671 2672 if not (self.ticker or self.figi): 2673 uLogger.error("Ticker or FIGI must be defined!") 2674 raise Exception("Ticker or FIGI required") 2675 2676 if self.ticker and not self.figi: 2677 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2678 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2679 2680 if self.figi and not self.ticker: 2681 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2682 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2683 2684 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2685 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2686 if interval.lower() != "day": 2687 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2688 2689 delta = dtEnd - dtStart # current UTC time minus last time in file 2690 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2691 2692 # calculate history length in candles: 2693 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2694 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2695 length += 1 # to avoid fraction time 2696 2697 # calculate data blocks count: 2698 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2699 2700 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2701 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2702 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2703 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2704 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2705 2706 tempOld = None # pandas object for old history, if --only-missing key present 2707 lastTime = None # datetime object of last old candle in file 2708 2709 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2710 uLogger.debug("--only-missing key present, add only last missing candles...") 2711 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2712 2713 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2714 2715 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2716 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2717 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2718 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2719 2720 # get last datetime object from last string in file or minus 1 delta if file is empty: 2721 if len(tempOld) > 0: 2722 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2723 2724 else: 2725 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2726 2727 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2728 2729 responseJSONs = [] # raw history blocks of data 2730 2731 blockEnd = dtEnd 2732 for item in range(blocks): 2733 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2734 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2735 2736 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2737 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2738 )) 2739 2740 if blockStart == blockEnd: 2741 uLogger.debug("Skipped this zero-length block...") 2742 2743 else: 2744 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2745 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2746 self.body = str({ 2747 "figi": self.figi, 2748 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2749 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2750 "interval": TKS_CANDLE_INTERVALS[interval][0] 2751 }) 2752 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2753 2754 if "code" in responseJSON.keys(): 2755 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2756 2757 else: 2758 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2759 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2760 2761 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2762 2763 blockEnd = blockStart 2764 2765 printCount = len(responseJSONs) # candles to show in console 2766 if responseJSONs: 2767 tempHistory = pd.DataFrame( 2768 data={ 2769 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2770 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2771 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2772 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2773 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2774 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2775 "volume": [int(item["volume"]) for item in responseJSONs], 2776 }, 2777 index=range(len(responseJSONs)), 2778 columns=["date", "time", "open", "high", "low", "close", "volume"], 2779 ) 2780 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2781 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2782 2783 # append only newest candles to old history if --only-missing key present: 2784 if onlyMissing and tempOld is not None and lastTime is not None: 2785 index = 0 # find start index in tempHistory data: 2786 2787 for i, item in tempHistory.iterrows(): 2788 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2789 2790 if curTime == lastTime: 2791 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2792 index = i 2793 printCount = index + 1 2794 break 2795 2796 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2797 2798 else: 2799 history = tempHistory # if no `--only-missing` key then load full data from server 2800 2801 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2802 2803 if history is not None and not history.empty: 2804 if show: 2805 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2806 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2807 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2808 )) 2809 2810 else: 2811 uLogger.warning("Received an empty candles history!") 2812 2813 if self.historyFile is not None: 2814 if history is not None and not history.empty: 2815 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2816 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2817 2818 else: 2819 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2820 2821 else: 2822 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2823 2824 return history 2825 2826 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2827 """ 2828 Load candles history from csv-file and return Pandas DataFrame object. 2829 2830 See also: `History()` and `ShowHistoryChart()` methods. 2831 2832 :param filePath: path to csv-file to open. 2833 """ 2834 loadedHistory = None # init candles data object 2835 2836 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2837 2838 if os.path.exists(filePath): 2839 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2840 2841 tfStr = self.priceModel.FormattedDelta( 2842 self.priceModel.timeframe, 2843 "{days} days {hours}h {minutes}m {seconds}s", 2844 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2845 self.priceModel.timeframe, 2846 "{hours}h {minutes}m {seconds}s", 2847 ) 2848 2849 if loadedHistory is not None and not loadedHistory.empty: 2850 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2851 len(loadedHistory), 2852 tfStr, 2853 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2854 ) 2855 2856 else: 2857 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2858 2859 else: 2860 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2861 2862 return loadedHistory 2863 2864 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2865 """ 2866 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2867 2868 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2869 Default: `index.html` (both for interact and non-interact candlesticks chart). 2870 2871 See also: `History()` and `LoadHistory()` methods. 2872 2873 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2874 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2875 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2876 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2877 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2878 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2879 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2880 """ 2881 if isinstance(candles, str): 2882 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2883 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2884 2885 elif isinstance(candles, pd.DataFrame): 2886 self.priceModel.prices = candles # set candles chain from variable 2887 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2888 2889 if "datetime" not in candles.columns: 2890 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2891 2892 else: 2893 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2894 raise Exception("Incorrect value") 2895 2896 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2897 2898 if interact: 2899 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2900 2901 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2902 2903 else: 2904 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2905 2906 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2907 2908 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2909 2910 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2911 """ 2912 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2913 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2914 2915 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2916 2917 :param operation: string "Buy" or "Sell". 2918 :param lots: volume, integer count of lots >= 1. 2919 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2920 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2921 :param expDate: string "Undefined" by default or local date in future, 2922 it is a string with format `%Y-%m-%d %H:%M:%S`. 2923 :return: JSON with response from broker server. 2924 """ 2925 if self.accountId is None or not self.accountId: 2926 uLogger.error("Variable `accountId` must be defined for using this method!") 2927 raise Exception("Account ID required") 2928 2929 if operation is None or not operation or operation not in ("Buy", "Sell"): 2930 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2931 raise Exception("Incorrect value") 2932 2933 if lots is None or lots < 1: 2934 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2935 lots = 1 2936 2937 if tp is None or tp < 0: 2938 tp = 0 2939 2940 if sl is None or sl < 0: 2941 sl = 0 2942 2943 if expDate is None or not expDate: 2944 expDate = "Undefined" 2945 2946 if not (self.ticker or self.figi): 2947 uLogger.error("Ticker or FIGI must be defined!") 2948 raise Exception("Ticker or FIGI required") 2949 2950 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2951 self.ticker = instrument["ticker"] 2952 self.figi = instrument["figi"] 2953 2954 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2955 2956 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2957 self.body = str({ 2958 "figi": self.figi, 2959 "quantity": str(lots), 2960 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2961 "accountId": str(self.accountId), 2962 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2963 }) 2964 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2965 2966 if "orderId" in response.keys(): 2967 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2968 operation, response["orderId"], 2969 self.ticker, self.figi, lots, 2970 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2971 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2972 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2973 )) 2974 2975 if tp > 0: 2976 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2977 2978 if sl > 0: 2979 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2980 2981 else: 2982 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2983 2984 return response 2985 2986 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2987 """ 2988 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2989 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2990 2991 See also: `Order()` and `Trade()` docstrings. 2992 2993 :param lots: volume, integer count of lots >= 1. 2994 :param tp: float > 0, take profit price of stop-order. 2995 :param sl: float > 0, stop loss price of stop-order. 2996 :param expDate: it's a local date in future. 2997 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2998 :return: JSON with response from broker server. 2999 """ 3000 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3001 3002 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3003 """ 3004 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3005 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3006 3007 See also: `Order()` and `Trade()` docstrings. 3008 3009 :param lots: volume, integer count of lots >= 1. 3010 :param tp: float > 0, take profit price of stop-order. 3011 :param sl: float > 0, stop loss price of stop-order. 3012 :param expDate: it's a local date in the future. 3013 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3014 :return: JSON with response from broker server. 3015 """ 3016 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3017 3018 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3019 """ 3020 Close position of given instruments. 3021 3022 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3023 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3024 This avoids unnecessary downloading data from the server. 3025 """ 3026 if instruments is None or not instruments: 3027 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3028 raise Exception("Ticker or FIGI required") 3029 3030 if isinstance(instruments, str): 3031 instruments = [instruments] 3032 3033 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3034 if uniqueInstruments: 3035 if portfolio is None or not portfolio: 3036 portfolio = self.Overview(show=False) 3037 3038 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3039 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3040 3041 for self.figi in uniqueInstruments: 3042 if self.figi not in allOpened: 3043 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3044 continue 3045 3046 # search open trade info about instrument by ticker: 3047 instrument = {} 3048 for iType in TKS_INSTRUMENTS: 3049 if instrument: 3050 break 3051 3052 for item in portfolio["stat"][iType]: 3053 if item["figi"] == self.figi: 3054 instrument = item 3055 break 3056 3057 if instrument: 3058 self.ticker = instrument["ticker"] 3059 self.figi = instrument["figi"] 3060 3061 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3062 self.ticker, 3063 self.figi, 3064 int(instrument["volume"]), 3065 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3066 )) 3067 3068 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3069 3070 if tradeLots > 0: 3071 if instrument["blocked"] > 0: 3072 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3073 instrument["blocked"], 3074 self.ticker, 3075 tradeLots, 3076 )) 3077 3078 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3079 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3080 3081 else: 3082 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3083 3084 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3085 """ 3086 Close all positions of given instruments with defined type. 3087 3088 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3089 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3090 This avoids unnecessary downloading data from the server. 3091 """ 3092 if iType not in TKS_INSTRUMENTS: 3093 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3094 3095 else: 3096 if portfolio is None or not portfolio: 3097 portfolio = self.Overview(show=False) 3098 3099 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3100 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3101 3102 if tickers and portfolio: 3103 self.CloseTrades(tickers, portfolio) 3104 3105 else: 3106 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3107 3108 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3109 """ 3110 Universal method to create market or limit orders with all available parameters for current `accountId`. 3111 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3112 3113 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3114 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3115 3116 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3117 then broker immediately open market order as you can do simple --buy or --sell operations! 3118 3119 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3120 When current price will go up or down to target price value then broker opens a limit order. 3121 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3122 3123 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3124 3125 :param operation: string "Buy" or "Sell". 3126 :param orderType: string "Limit" or "Stop". 3127 :param lots: volume, integer count of lots >= 1. 3128 :param targetPrice: target price > 0. This is open trade price for limit order. 3129 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3130 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3131 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3132 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3133 Stop loss order always executed by market price. 3134 :param expDate: string "Undefined" by default or local date in future. 3135 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3136 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3137 A limit order has no expiration date, it lasts until the end of the trading day. 3138 :return: JSON with response from broker server. 3139 """ 3140 if self.accountId is None or not self.accountId: 3141 uLogger.error("Variable `accountId` must be defined for using this method!") 3142 raise Exception("Account ID required") 3143 3144 if operation is None or not operation or operation not in ("Buy", "Sell"): 3145 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3146 raise Exception("Incorrect value") 3147 3148 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3149 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3150 raise Exception("Incorrect value") 3151 3152 if lots is None or lots < 1: 3153 uLogger.error("You must define trade volume > 0: integer count of lots!") 3154 raise Exception("Incorrect value") 3155 3156 if targetPrice is None or targetPrice <= 0: 3157 uLogger.error("Target price for limit-order must be greater than 0!") 3158 raise Exception("Incorrect value") 3159 3160 if limitPrice is None or limitPrice <= 0: 3161 limitPrice = targetPrice 3162 3163 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3164 stopType = "Limit" 3165 3166 if expDate is None or not expDate: 3167 expDate = "Undefined" 3168 3169 if not (self.ticker or self.figi): 3170 uLogger.error("Tocker or FIGI must be defined!") 3171 raise Exception("Ticker or FIGI required") 3172 3173 response = {} 3174 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3175 self.ticker = instrument["ticker"] 3176 self.figi = instrument["figi"] 3177 3178 if orderType == "Limit": 3179 uLogger.debug( 3180 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3181 self.ticker, self.figi, 3182 operation, lots, targetPrice, instrument["currency"], 3183 )) 3184 3185 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3186 self.body = str({ 3187 "figi": self.figi, 3188 "quantity": str(lots), 3189 "price": FloatToNano(targetPrice), 3190 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3191 "accountId": str(self.accountId), 3192 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3193 }) 3194 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3195 3196 if "orderId" in response.keys(): 3197 uLogger.info( 3198 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3199 response["orderId"], 3200 self.ticker, self.figi, 3201 operation, lots, targetPrice, instrument["currency"], 3202 )) 3203 3204 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3205 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3206 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3207 targetPrice, instrument["currency"], 3208 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3209 )) 3210 3211 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3212 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3213 targetPrice, instrument["currency"], 3214 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3215 )) 3216 3217 else: 3218 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3219 3220 if orderType == "Stop": 3221 uLogger.debug( 3222 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3223 self.ticker, self.figi, 3224 operation, lots, 3225 targetPrice, instrument["currency"], 3226 limitPrice, instrument["currency"], 3227 stopType, expDate, 3228 )) 3229 3230 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3231 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3232 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3233 3234 body = { 3235 "figi": self.figi, 3236 "quantity": str(lots), 3237 "price": FloatToNano(limitPrice), 3238 "stopPrice": FloatToNano(targetPrice), 3239 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3240 "accountId": str(self.accountId), 3241 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3242 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3243 } 3244 3245 if expDateUTC: 3246 body["expireDate"] = expDateUTC 3247 3248 self.body = str(body) 3249 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3250 3251 if "stopOrderId" in response.keys(): 3252 uLogger.info( 3253 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3254 response["stopOrderId"], 3255 self.ticker, self.figi, 3256 operation, lots, 3257 targetPrice, instrument["currency"], 3258 limitPrice, instrument["currency"], 3259 TKS_STOP_ORDER_TYPES[stopOrderType], 3260 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3261 )) 3262 3263 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3264 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3265 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3266 targetPrice, instrument["currency"], 3267 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3268 )) 3269 3270 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3271 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3272 targetPrice, instrument["currency"], 3273 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3274 )) 3275 3276 else: 3277 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3278 3279 return response 3280 3281 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3282 """ 3283 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3284 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3285 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3286 See also: `Order()` docstring. 3287 3288 :param lots: volume, integer count of lots >= 1. 3289 :param targetPrice: target price > 0. This is open trade price for limit order. 3290 :return: JSON with response from broker server. 3291 """ 3292 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3293 3294 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3295 """ 3296 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3297 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3298 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3299 target price value then broker opens a limit order. See also: `Order()` docstring. 3300 3301 :param lots: volume, integer count of lots >= 1. 3302 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3303 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3304 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3305 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3306 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3307 :param expDate: string "Undefined" by default or local date in future. 3308 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3309 This date is converting to UTC format for server. 3310 :return: JSON with response from broker server. 3311 """ 3312 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3313 3314 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3315 """ 3316 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3317 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3318 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3319 See also: `Order()` docstring. 3320 3321 :param lots: volume, integer count of lots >= 1. 3322 :param targetPrice: target price > 0. This is open trade price for limit order. 3323 :return: JSON with response from broker server. 3324 """ 3325 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3326 3327 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3328 """ 3329 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3330 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3331 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3332 target price value then broker opens a limit order. See also: `Order()` docstring. 3333 3334 :param lots: volume, integer count of lots >= 1. 3335 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3336 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3337 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3338 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3339 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3340 :param expDate: string "Undefined" by default or local date in future. 3341 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3342 This date is converting to UTC format for server. 3343 :return: JSON with response from broker server. 3344 """ 3345 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3346 3347 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3348 """ 3349 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3350 3351 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3352 :param allOrdersIDs: pre-received lists of all active pending orders. 3353 This avoids unnecessary downloading data from the server. 3354 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3355 """ 3356 if self.accountId is None or not self.accountId: 3357 uLogger.error("Variable `accountId` must be defined for using this method!") 3358 raise Exception("Account ID required") 3359 3360 if orderIDs: 3361 if allOrdersIDs is None or not allOrdersIDs: 3362 rawOrders = self.RequestPendingOrders() 3363 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3364 3365 if allStopOrdersIDs is None or not allStopOrdersIDs: 3366 rawStopOrders = self.RequestStopOrders() 3367 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3368 3369 for orderID in orderIDs: 3370 idInPendingOrders = orderID in allOrdersIDs 3371 idInStopOrders = orderID in allStopOrdersIDs 3372 3373 if not (idInPendingOrders or idInStopOrders): 3374 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3375 continue 3376 3377 else: 3378 if idInPendingOrders: 3379 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3380 3381 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3382 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3383 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3384 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3385 3386 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3387 if self.moreDebug: 3388 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3389 3390 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3391 3392 else: 3393 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3394 3395 elif idInStopOrders: 3396 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3397 3398 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3399 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3400 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3401 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3402 3403 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3404 if self.moreDebug: 3405 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3406 3407 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3408 3409 else: 3410 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3411 3412 else: 3413 continue 3414 3415 def CloseAllOrders(self) -> None: 3416 """ 3417 Gets a list of open pending and stop orders and cancel it all. 3418 """ 3419 rawOrders = self.RequestPendingOrders() 3420 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3421 lenOrders = len(allOrdersIDs) 3422 3423 rawStopOrders = self.RequestStopOrders() 3424 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3425 lenSOrders = len(allStopOrdersIDs) 3426 3427 if lenOrders > 0 or lenSOrders > 0: 3428 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3429 3430 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3431 3432 else: 3433 uLogger.info("Orders not found, nothing to cancel.") 3434 3435 def CloseAll(self, *args) -> None: 3436 """ 3437 Close all available (not blocked) opened trades and orders. 3438 3439 Also, you can select one or more keywords case-insensitive: 3440 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3441 3442 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3443 """ 3444 overview = self.Overview(show=False) # get all open trades info 3445 3446 if len(args) == 0: 3447 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3448 self.CloseAllOrders() # close all pending and stop orders 3449 3450 for iType in TKS_INSTRUMENTS: 3451 if iType != "Currencies": 3452 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3453 3454 else: 3455 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3456 lowerArgs = [x.lower() for x in args] 3457 3458 if "orders" in lowerArgs: 3459 self.CloseAllOrders() # close all pending and stop orders 3460 3461 for iType in TKS_INSTRUMENTS: 3462 if iType.lower() in lowerArgs and iType != "Currencies": 3463 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3464 3465 @staticmethod 3466 def ParseOrderParameters(operation, **inputParameters): 3467 """ 3468 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3469 3470 :param operation: string "Buy" or "Sell". 3471 :param inputParameters: this is dict of strings that looks like this 3472 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3473 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3474 "prices" key: one or more prices to open limit-orders 3475 Counts of values in lots and prices lists must be equals! 3476 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3477 """ 3478 # TODO: update order grid work with api v2 3479 pass 3480 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3481 # 3482 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3483 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3484 # raise Exception("Incorrect value") 3485 # 3486 # if "l" in inputParameters.keys(): 3487 # inputParameters["lots"] = inputParameters.pop("l") 3488 # 3489 # if "p" in inputParameters.keys(): 3490 # inputParameters["prices"] = inputParameters.pop("p") 3491 # 3492 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3493 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3494 # raise Exception("Incorrect value") 3495 # 3496 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3497 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3498 # 3499 # if len(lots) != len(prices): 3500 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3501 # raise Exception("Incorrect value") 3502 # 3503 # uLogger.debug("Extracted parameters for orders:") 3504 # uLogger.debug("lots = {}".format(lots)) 3505 # uLogger.debug("prices = {}".format(prices)) 3506 # 3507 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3508 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3509 # uLogger.debug("Order parameters: {}".format(result)) 3510 # 3511 # return result 3512 3513 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3514 """ 3515 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3516 3517 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3518 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3519 """ 3520 result = False 3521 msg = "Instrument not defined!" 3522 3523 if portfolio is None or not portfolio: 3524 portfolio = self.Overview(show=False) 3525 3526 if self.ticker: 3527 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3528 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3529 3530 for iType in TKS_INSTRUMENTS: 3531 for instrument in portfolio["stat"][iType]: 3532 if instrument["ticker"] == self.ticker: 3533 result = True 3534 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3535 break 3536 3537 elif self.figi: 3538 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3539 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3540 3541 for iType in TKS_INSTRUMENTS: 3542 for instrument in portfolio["stat"][iType]: 3543 if instrument["figi"] == self.figi: 3544 result = True 3545 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3546 break 3547 3548 else: 3549 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3550 3551 uLogger.debug(msg) 3552 3553 return result 3554 3555 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3556 """ 3557 Returns instrument is in the user's portfolio if it presents there. 3558 Instrument must be defined by `ticker` (highly priority) or `figi`. 3559 3560 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3561 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3562 """ 3563 result = None 3564 msg = "Instrument not defined!" 3565 3566 if portfolio is None or not portfolio: 3567 portfolio = self.Overview(show=False) 3568 3569 if self.ticker: 3570 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3571 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3572 3573 for iType in TKS_INSTRUMENTS: 3574 for instrument in portfolio["stat"][iType]: 3575 if instrument["ticker"] == self.ticker: 3576 result = instrument 3577 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3578 break 3579 3580 elif self.figi: 3581 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3582 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3583 3584 for iType in TKS_INSTRUMENTS: 3585 for instrument in portfolio["stat"][iType]: 3586 if instrument["figi"] == self.figi: 3587 result = instrument 3588 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3589 break 3590 3591 else: 3592 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3593 3594 uLogger.debug(msg) 3595 3596 return result 3597 3598 def RequestLimits(self) -> dict: 3599 """ 3600 Method for obtaining the available funds for withdrawal for current `accountId`. 3601 3602 See also: 3603 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3604 - `OverviewLimits()` method 3605 3606 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3607 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3608 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3609 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3610 """ 3611 if self.accountId is None or not self.accountId: 3612 uLogger.error("Variable `accountId` must be defined for using this method!") 3613 raise Exception("Account ID required") 3614 3615 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3616 3617 self.body = str({"accountId": self.accountId}) 3618 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3619 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3620 3621 if self.moreDebug: 3622 uLogger.debug("Records about available funds for withdrawal successfully received") 3623 3624 return rawLimits 3625 3626 def OverviewLimits(self, show: bool = False) -> dict: 3627 """ 3628 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3629 3630 See also: `RequestLimits()`. 3631 3632 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3633 :return: dict with raw parsed data from server and some calculated statistics about it. 3634 """ 3635 if self.accountId is None or not self.accountId: 3636 uLogger.error("Variable `accountId` must be defined for using this method!") 3637 raise Exception("Account ID required") 3638 3639 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3640 3641 view = { 3642 "rawLimits": rawLimits, 3643 "limits": { # parsed data for every currency: 3644 "money": { # this is an array of portfolio currency positions 3645 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3646 }, 3647 "blocked": { # this is an array of blocked currency 3648 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3649 }, 3650 "blockedGuarantee": { # this is locked money under collateral for futures 3651 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3652 }, 3653 }, 3654 } 3655 3656 # --- Prepare text table with limits in human-readable format: 3657 if show: 3658 info = [ 3659 "# Withdrawal limits\n\n", 3660 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3661 "* **Account ID:** [{}]\n".format(self.accountId), 3662 ] 3663 3664 if view["limits"]["money"]: 3665 info.extend([ 3666 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3667 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3668 ]) 3669 3670 else: 3671 info.append("\nNo withdrawal limits\n") 3672 3673 for curr in view["limits"]["money"].keys(): 3674 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3675 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3676 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3677 3678 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3679 "[{}]".format(curr), 3680 "{:.2f}".format(view["limits"]["money"][curr]), 3681 "{:.2f}".format(availableMoney), 3682 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3683 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3684 ) 3685 3686 if curr == "rub": 3687 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3688 3689 else: 3690 info.append(infoStr) 3691 3692 infoText = "".join(info) 3693 3694 uLogger.info(infoText) 3695 3696 if self.withdrawalLimitsFile: 3697 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3698 fH.write(infoText) 3699 3700 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3701 3702 return view 3703 3704 def RequestAccounts(self) -> dict: 3705 """ 3706 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3707 3708 See also: 3709 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3710 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3711 - `OverviewUserInfo()` method 3712 3713 :return: dict with raw data from server that contains accounts info. Example of dict: 3714 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3715 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3716 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3717 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3718 """ 3719 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3720 3721 self.body = str({}) 3722 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3723 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3724 3725 if self.moreDebug: 3726 uLogger.debug("Records about available accounts successfully received") 3727 3728 return rawAccounts 3729 3730 def RequestUserInfo(self) -> dict: 3731 """ 3732 Method for requesting common user's information. 3733 3734 See also: 3735 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3736 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3737 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3738 - `OverviewUserInfo()` method 3739 3740 :return: dict with raw data from server that contains user's information. Example of dict: 3741 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3742 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3743 """ 3744 uLogger.debug("Requesting common user's information. Wait, please...") 3745 3746 self.body = str({}) 3747 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3748 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3749 3750 if self.moreDebug: 3751 uLogger.debug("Records about current user successfully received") 3752 3753 return rawUserInfo 3754 3755 def RequestMarginStatus(self, accountId: str = None) -> dict: 3756 """ 3757 Method for requesting margin calculation for defined account ID. 3758 3759 See also: 3760 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3761 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3762 - `OverviewUserInfo()` method 3763 3764 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3765 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3766 Example of responses: 3767 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3768 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3769 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3770 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3771 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3772 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3773 """ 3774 if accountId is None or not accountId: 3775 if self.accountId is None or not self.accountId: 3776 uLogger.error("Variable `accountId` must be defined for using this method!") 3777 raise Exception("Account ID required") 3778 3779 else: 3780 accountId = self.accountId # use `self.accountId` (main ID) by default 3781 3782 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3783 3784 self.body = str({"accountId": accountId}) 3785 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3786 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3787 3788 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3789 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3790 rawMargin = {} 3791 3792 else: 3793 if self.moreDebug: 3794 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3795 3796 return rawMargin 3797 3798 def RequestTariffLimits(self) -> dict: 3799 """ 3800 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3801 3802 See also: 3803 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3804 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3805 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3806 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3807 - `OverviewUserInfo()` method 3808 3809 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3810 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3811 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3812 """ 3813 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3814 3815 self.body = str({}) 3816 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3817 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3818 3819 if self.moreDebug: 3820 uLogger.debug("Records with limits of current tariff successfully received") 3821 3822 return rawTariffLimits 3823 3824 def RequestBondCoupons(self, iJSON: dict) -> dict: 3825 """ 3826 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3827 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3828 All dates are in UTC timezone. 3829 3830 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3831 Documentation: 3832 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3833 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3834 3835 See also: `ExtendBondsData()`. 3836 3837 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3838 If raw iJSON is not data of bond then server returns an error [400] with message: 3839 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3840 :return: dictionary with bond payment calendar. Response example 3841 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3842 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3843 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3844 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3845 """ 3846 if iJSON["figi"] is None or not iJSON["figi"]: 3847 uLogger.error("FIGI must be defined for using this method!") 3848 raise Exception("FIGI required") 3849 3850 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3851 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3852 3853 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3854 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3855 self.figi, 3856 startDate, 3857 endDate, 3858 )) 3859 3860 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3861 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3862 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3863 3864 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3865 uLogger.warning("Instrument type is not bond!") 3866 3867 else: 3868 if self.moreDebug: 3869 uLogger.debug("Records about bond payment calendar successfully received") 3870 3871 return calendar 3872 3873 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3874 """ 3875 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3876 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3877 coupon yields, current yields and some statistics etc. 3878 3879 WARNING! This is too long operation if a lot of bonds requested from broker server. 3880 3881 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3882 3883 :param instruments: list of strings with tickers or FIGIs. 3884 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3885 for further used by data scientists or stock analytics. 3886 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3887 In XLSX-file and Pandas DataFrame fields mean: 3888 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3889 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3890 """ 3891 if instruments is None or not instruments: 3892 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3893 raise Exception("Ticker or FIGI required") 3894 3895 if isinstance(instruments, str): 3896 instruments = [instruments] 3897 3898 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3899 3900 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3901 3902 iCount = len(uniqueInstruments) 3903 tooLong = iCount >= 20 3904 if tooLong: 3905 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3906 3907 bonds = None 3908 for i, self.figi in enumerate(uniqueInstruments): 3909 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3910 3911 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3912 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3913 rawBond = self.SearchByFIGI(requestPrice=True) 3914 3915 # Widen raw data with UTC current time (iData["actualDateTime"]): 3916 actualDate = datetime.now(tzutc()) 3917 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3918 3919 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3920 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3921 3922 # Replace some values with human-readable: 3923 iData["nominalCurrency"] = iData["nominal"]["currency"] 3924 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3925 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3926 iData["aciCurrency"] = iData["aciValue"]["currency"] 3927 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3928 iData["issueSize"] = int(iData["issueSize"]) 3929 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3930 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3931 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3932 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3933 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3934 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3935 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3936 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3937 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3938 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3939 3940 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3941 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3942 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3943 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3944 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3945 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3946 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3947 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3948 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3949 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3950 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3951 3952 # Widen raw data with calendar data from `rawCalendar` values: 3953 calendarData = [] 3954 if "events" in iData["rawCalendar"].keys(): 3955 for item in iData["rawCalendar"]["events"]: 3956 calendarData.append({ 3957 "couponDate": item["couponDate"], 3958 "couponNumber": int(item["couponNumber"]), 3959 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3960 "payCurrency": item["payOneBond"]["currency"], 3961 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3962 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3963 "couponStartDate": item["couponStartDate"], 3964 "couponEndDate": item["couponEndDate"], 3965 "couponPeriod": item["couponPeriod"], 3966 }) 3967 3968 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3969 if "maturityDate" not in iData.keys(): 3970 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3971 3972 # Widen raw data with Coupon Rate. 3973 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3974 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3975 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3976 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3977 3978 # Widen raw data with Yield to Maturity (YTM) on current date. 3979 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3980 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3981 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3982 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3983 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3984 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3985 3986 iData["calendar"] = calendarData # adds calendar at the end 3987 3988 # Remove not used data: 3989 iData.pop("uid") 3990 iData.pop("positionUid") 3991 iData.pop("currentPrice") 3992 iData.pop("rawCalendar") 3993 3994 colNames = list(iData.keys()) 3995 if bonds is None: 3996 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3997 3998 else: 3999 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4000 4001 else: 4002 uLogger.warning("Instrument is not a bond!") 4003 4004 processed = round(100 * (i + 1) / iCount, 1) 4005 if tooLong and processed % 5 == 0: 4006 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4007 4008 else: 4009 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4010 4011 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4012 4013 # Saving bonds from Pandas DataFrame to XLSX sheet: 4014 if xlsx and self.bondsXLSXFile: 4015 with pd.ExcelWriter( 4016 path=self.bondsXLSXFile, 4017 date_format=TKS_DATE_FORMAT, 4018 datetime_format=TKS_DATE_TIME_FORMAT, 4019 mode="w", 4020 ) as writer: 4021 bonds.to_excel( 4022 writer, 4023 sheet_name="Extended bonds data", 4024 index=True, 4025 encoding="UTF-8", 4026 freeze_panes=(1, 1), 4027 ) # saving as XLSX-file with freeze first row and column as headers 4028 4029 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4030 4031 return bonds 4032 4033 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4034 """ 4035 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4036 4037 WARNING! This is too long operation if a lot of bonds requested from broker server. 4038 4039 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4040 4041 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4042 extended information about bonds: main info, current prices, bond payment calendar, 4043 coupon yields, current yields and some statistics etc. 4044 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4045 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4046 for further used by data scientists or stock analytics. 4047 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4048 """ 4049 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4050 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4051 4052 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4053 4054 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4055 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4056 calendar = None 4057 for bond in extBonds.iterrows(): 4058 for item in bond[1]["calendar"]: 4059 cData = { 4060 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4061 "couponDate": item["couponDate"], 4062 "figi": bond[1]["figi"], 4063 "ticker": bond[1]["ticker"], 4064 "name": bond[1]["name"], 4065 "couponNumber": item["couponNumber"], 4066 "payOneBond": item["payOneBond"], 4067 "payCurrency": item["payCurrency"], 4068 "couponType": item["couponType"], 4069 "couponPeriod": item["couponPeriod"], 4070 "fixDate": item["fixDate"], 4071 "couponStartDate": item["couponStartDate"], 4072 "couponEndDate": item["couponEndDate"], 4073 } 4074 4075 if calendar is None: 4076 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4077 4078 else: 4079 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4080 4081 if calendar is not None: 4082 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4083 4084 # Saving calendar from Pandas DataFrame to XLSX sheet: 4085 if xlsx: 4086 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4087 4088 with pd.ExcelWriter( 4089 path=xlsxCalendarFile, 4090 date_format=TKS_DATE_FORMAT, 4091 datetime_format=TKS_DATE_TIME_FORMAT, 4092 mode="w", 4093 ) as writer: 4094 humanReadable = calendar.copy(deep=True) 4095 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4096 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4097 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4098 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4099 humanReadable.columns = colNames # human-readable column names 4100 4101 humanReadable.to_excel( 4102 writer, 4103 sheet_name="Bond payments calendar", 4104 index=False, 4105 encoding="UTF-8", 4106 freeze_panes=(1, 2), 4107 ) # saving as XLSX-file with freeze first row and column as headers 4108 4109 del humanReadable # release df in memory 4110 4111 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4112 4113 return calendar 4114 4115 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4116 """ 4117 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4118 Also, creates Markdown file with calendar data, `calendar.md` by default. 4119 4120 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4121 4122 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4123 extended information about bonds: main info, current prices, bond payment calendar, 4124 coupon yields, current yields and some statistics etc. 4125 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4126 :param show: if `True` then also printing bonds payment calendar to the console, 4127 otherwise save to file `calendarFile` only. `False` by default. 4128 :return: multilines text in Markdown format with bonds payment calendar as a table. 4129 """ 4130 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4131 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4132 4133 infoText = "# Bond payments calendar\n\n" 4134 4135 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4136 4137 if not (calendar is None or calendar.empty): 4138 splitLine = "| | | | | | | | | |\n" 4139 4140 info = [ 4141 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4142 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4143 ] 4144 4145 newMonth = False 4146 notOneBond = calendar["figi"].nunique() > 1 4147 for i, bond in enumerate(calendar.iterrows()): 4148 if newMonth and notOneBond: 4149 info.append(splitLine) 4150 4151 info.append( 4152 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4153 " √" if bond[1]["paid"] else " —", 4154 bond[1]["couponDate"].split("T")[0], 4155 bond[1]["figi"], 4156 bond[1]["ticker"], 4157 bond[1]["couponNumber"], 4158 "{} {}".format( 4159 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4160 bond[1]["payCurrency"], 4161 ), 4162 bond[1]["couponType"], 4163 bond[1]["couponPeriod"], 4164 bond[1]["fixDate"].split("T")[0], 4165 ) 4166 ) 4167 4168 if i < len(calendar.values) - 1: 4169 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4170 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4171 newMonth = False if curDate.month == nextDate.month else True 4172 4173 else: 4174 newMonth = False 4175 4176 infoText += "".join(info) 4177 4178 if show: 4179 uLogger.info("{}".format(infoText)) 4180 4181 if self.calendarFile is not None: 4182 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4183 fH.write(infoText) 4184 4185 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4186 4187 else: 4188 infoText += "No data\n" 4189 4190 return infoText 4191 4192 def OverviewAccounts(self, show: bool = False) -> dict: 4193 """ 4194 Method for parsing and show simple table with all available user accounts. 4195 4196 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4197 4198 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4199 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4200 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4201 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4202 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4203 "closed": "—", "access": "Full access" }, ...}}` 4204 """ 4205 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4206 4207 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4208 accounts = { 4209 item["id"]: { 4210 "type": TKS_ACCOUNT_TYPES[item["type"]], 4211 "name": item["name"], 4212 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4213 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4214 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4215 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4216 } for item in rawAccounts["accounts"] 4217 } 4218 4219 # Raw and parsed data with some fields replaced in "stat" section: 4220 view = { 4221 "rawAccounts": rawAccounts, 4222 "stat": accounts, 4223 } 4224 4225 # --- Prepare simple text table with only accounts data in human-readable format: 4226 if show: 4227 info = [ 4228 "# User accounts\n\n", 4229 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4230 "| Account ID | Type | Status | Name |\n", 4231 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4232 ] 4233 4234 for account in view["stat"].keys(): 4235 info.extend([ 4236 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4237 account, 4238 view["stat"][account]["type"], 4239 view["stat"][account]["status"], 4240 view["stat"][account]["name"], 4241 ) 4242 ]) 4243 4244 infoText = "".join(info) 4245 4246 uLogger.info(infoText) 4247 4248 if self.userAccountsFile: 4249 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4250 fH.write(infoText) 4251 4252 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4253 4254 return view 4255 4256 def OverviewUserInfo(self, show: bool = False) -> dict: 4257 """ 4258 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4259 4260 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4261 4262 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4263 :return: dict with raw parsed data from server and some calculated statistics about it. 4264 """ 4265 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4266 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4267 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4268 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4269 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4270 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4271 4272 # This is dict with parsed common user data: 4273 userInfo = { 4274 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4275 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4276 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4277 "tariff": rawUserInfo["tariff"], 4278 } 4279 4280 # This is an array of dict with parsed margin statuses for every account IDs: 4281 margins = {} 4282 for accountId in accounts.keys(): 4283 if rawMargins[accountId]: 4284 margins[accountId] = { 4285 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4286 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4287 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4288 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4289 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4290 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4291 } 4292 4293 else: 4294 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4295 4296 unary = {} # unary-connection limits 4297 for item in rawTariffLimits["unaryLimits"]: 4298 if item["limitPerMinute"] in unary.keys(): 4299 unary[item["limitPerMinute"]].extend(item["methods"]) 4300 4301 else: 4302 unary[item["limitPerMinute"]] = item["methods"] 4303 4304 stream = {} # stream-connection limits 4305 for item in rawTariffLimits["streamLimits"]: 4306 if item["limit"] in stream.keys(): 4307 stream[item["limit"]].extend(item["streams"]) 4308 4309 else: 4310 stream[item["limit"]] = item["streams"] 4311 4312 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4313 limits = { 4314 "unary": unary, 4315 "stream": stream, 4316 } 4317 4318 # Raw and parsed data as an output result: 4319 view = { 4320 "rawUserInfo": rawUserInfo, 4321 "rawAccounts": rawAccounts, 4322 "rawMargins": rawMargins, 4323 "rawTariffLimits": rawTariffLimits, 4324 "stat": { 4325 "userInfo": userInfo, 4326 "accounts": accounts, 4327 "margins": margins, 4328 "limits": limits, 4329 }, 4330 } 4331 4332 # --- Prepare text table with user information in human-readable format: 4333 if show: 4334 info = [ 4335 "# Full user information\n\n", 4336 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4337 "## Common information\n\n", 4338 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4339 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4340 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4341 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4342 "\n## User accounts\n\n", 4343 ] 4344 4345 for account in view["stat"]["accounts"].keys(): 4346 info.extend([ 4347 "### ID: [{}]\n\n".format(account), 4348 "| Parameters | Values |\n", 4349 "|----------------------|--------------------------------------------------------------|\n", 4350 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4351 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4352 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4353 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4354 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4355 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4356 ]) 4357 4358 if margins[account]: 4359 info.extend([ 4360 "| Margin status: | Enabled |\n", 4361 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4362 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4363 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4364 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4365 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4366 ]) 4367 4368 else: 4369 info.append("| Margin status: | Disabled |\n\n") 4370 4371 info.extend([ 4372 "\n## Current user tariff limits\n", 4373 "\nSee also:\n", 4374 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4375 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4376 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4377 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4378 "\n### Unary limits\n", 4379 ]) 4380 4381 if unary: 4382 for key, values in sorted(unary.items()): 4383 info.append("\n* Max requests per minute: {}\n".format(key)) 4384 4385 for value in values: 4386 info.append(" - {}\n".format(value)) 4387 4388 else: 4389 info.append("\nNot available\n") 4390 4391 info.append("\n### Stream limits\n") 4392 4393 if stream: 4394 for key, values in sorted(stream.items()): 4395 info.append("\n* Max stream connections: {}\n".format(key)) 4396 4397 for value in values: 4398 info.append(" - {}\n".format(value)) 4399 4400 else: 4401 info.append("\nNot available\n") 4402 4403 infoText = "".join(info) 4404 4405 uLogger.info(infoText) 4406 4407 if self.userInfoFile: 4408 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4409 fH.write(infoText) 4410 4411 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4412 4413 return view 4414 4415 4416class Args: 4417 """ 4418 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4419 """ 4420 def __init__(self, **kwargs): 4421 self.__dict__.update(kwargs) 4422 4423 def __getattr__(self, item): 4424 return None 4425 4426 4427def ParseArgs(): 4428 """This function get and parse command line keys.""" 4429 parser = ArgumentParser() # command-line string parser 4430 4431 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4432 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4433 4434 # --- options: 4435 4436 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4437 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4438 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4439 4440 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4441 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4442 4443 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4444 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4445 4446 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4447 4448 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4449 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4450 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4451 4452 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4453 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4454 4455 # --- commands: 4456 4457 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4458 4459 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4460 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4461 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4462 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4463 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4464 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4465 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4466 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4467 4468 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4469 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4470 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4471 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4472 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4473 4474 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4475 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4476 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4477 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4478 4479 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4480 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4481 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4482 4483 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4484 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4485 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4486 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4487 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4488 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4489 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4490 4491 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4492 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4493 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4494 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4495 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4496 4497 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4498 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4499 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4500 4501 cmdArgs = parser.parse_args() 4502 return cmdArgs 4503 4504 4505def Main(**kwargs): 4506 """ 4507 Main function for work with TKSBrokerAPI in the console. 4508 4509 See examples: 4510 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4511 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4512 """ 4513 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4514 4515 if args.debug_level: 4516 uLogger.level = 10 # always debug level by default 4517 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4518 4519 exitCode = 0 4520 start = datetime.now(tzutc()) 4521 uLogger.debug("=-" * 50) 4522 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4523 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4524 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4525 )) 4526 4527 # trying to calculate full current version: 4528 buildVersion = __version__ 4529 try: 4530 v = version("tksbrokerapi") 4531 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4532 4533 except Exception: 4534 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4535 4536 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4537 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4538 4539 try: 4540 if args.version: 4541 print("TKSBrokerAPI {}".format(buildVersion)) 4542 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4543 4544 else: 4545 # Init class for trading with Tinkoff Broker: 4546 trader = TinkoffBrokerServer( 4547 token=args.token, 4548 accountId=args.account_id, 4549 useCache=not args.no_cache, 4550 ) 4551 4552 # --- set some options: 4553 4554 if args.more: 4555 trader.moreDebug = True 4556 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4557 4558 if args.ticker: 4559 if args.ticker in trader.aliasesKeys: 4560 trader.ticker = trader.aliases[args.ticker] # Replace some tickers with its aliases 4561 4562 else: 4563 trader.ticker = args.ticker 4564 4565 if args.figi: 4566 trader.figi = args.figi 4567 4568 if args.depth is not None: 4569 trader.depth = args.depth 4570 4571 # --- do one command: 4572 4573 if args.list: 4574 if args.output is not None: 4575 trader.instrumentsFile = args.output 4576 4577 trader.ShowInstrumentsInfo(show=True) 4578 4579 elif args.list_xlsx: 4580 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4581 4582 elif args.bonds_xlsx is not None: 4583 if args.output is not None: 4584 trader.bondsXLSXFile = args.output 4585 4586 if len(args.bonds_xlsx) == 0: 4587 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4588 4589 else: 4590 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4591 4592 elif args.search: 4593 if args.output is not None: 4594 trader.searchResultsFile = args.output 4595 4596 trader.SearchInstruments(pattern=args.search[0], show=True) 4597 4598 elif args.info: 4599 if not (args.ticker or args.figi): 4600 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4601 raise Exception("Ticker or FIGI required") 4602 4603 if args.output is not None: 4604 trader.infoFile = args.output 4605 4606 if args.ticker: 4607 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4608 4609 else: 4610 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4611 4612 elif args.calendar is not None: 4613 if args.output is not None: 4614 trader.calendarFile = args.output 4615 4616 if len(args.calendar) == 0: 4617 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4618 4619 else: 4620 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4621 4622 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4623 4624 elif args.price: 4625 if not (args.ticker or args.figi): 4626 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4627 raise Exception("Ticker or FIGI required") 4628 4629 trader.GetCurrentPrices(show=True) 4630 4631 elif args.prices is not None: 4632 if args.output is not None: 4633 trader.pricesFile = args.output 4634 4635 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4636 4637 elif args.overview: 4638 if args.output is not None: 4639 trader.overviewFile = args.output 4640 4641 trader.Overview(show=True, details="full") 4642 4643 elif args.overview_digest: 4644 if args.output is not None: 4645 trader.overviewDigestFile = args.output 4646 4647 trader.Overview(show=True, details="digest") 4648 4649 elif args.overview_positions: 4650 if args.output is not None: 4651 trader.overviewPositionsFile = args.output 4652 4653 trader.Overview(show=True, details="positions") 4654 4655 elif args.overview_orders: 4656 if args.output is not None: 4657 trader.overviewOrdersFile = args.output 4658 4659 trader.Overview(show=True, details="orders") 4660 4661 elif args.overview_analytics: 4662 if args.output is not None: 4663 trader.overviewAnalyticsFile = args.output 4664 4665 trader.Overview(show=True, details="analytics") 4666 4667 elif args.deals is not None: 4668 if args.output is not None: 4669 trader.reportFile = args.output 4670 4671 if 0 <= len(args.deals) < 3: 4672 trader.Deals( 4673 start=args.deals[0] if len(args.deals) >= 1 else None, 4674 end=args.deals[1] if len(args.deals) == 2 else None, 4675 show=True, # Always show deals report in console 4676 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4677 ) 4678 4679 else: 4680 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4681 raise Exception("Incorrect value") 4682 4683 elif args.history is not None: 4684 if args.output is not None: 4685 trader.historyFile = args.output 4686 4687 if 0 <= len(args.history) < 3: 4688 dataReceived = trader.History( 4689 start=args.history[0] if len(args.history) >= 1 else None, 4690 end=args.history[1] if len(args.history) == 2 else None, 4691 interval="hour" if args.interval is None or not args.interval else args.interval, 4692 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4693 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4694 show=True, # shows all downloaded candles in console 4695 ) 4696 4697 if args.render_chart is not None and dataReceived is not None: 4698 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4699 4700 trader.ShowHistoryChart( 4701 candles=dataReceived, 4702 interact=iChart, 4703 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4704 ) 4705 4706 else: 4707 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4708 raise Exception("Incorrect value") 4709 4710 elif args.load_history is not None: 4711 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4712 4713 if args.render_chart is not None and histData is not None: 4714 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4715 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4716 4717 trader.ShowHistoryChart( 4718 candles=histData, 4719 interact=iChart, 4720 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4721 ) 4722 4723 elif args.trade is not None: 4724 if 1 <= len(args.trade) <= 5: 4725 trader.Trade( 4726 operation=args.trade[0], 4727 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4728 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4729 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4730 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4731 ) 4732 4733 else: 4734 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4735 4736 elif args.buy is not None: 4737 if 0 <= len(args.buy) <= 4: 4738 trader.Buy( 4739 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4740 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4741 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4742 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4743 ) 4744 4745 else: 4746 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4747 4748 elif args.sell is not None: 4749 if 0 <= len(args.sell) <= 4: 4750 trader.Sell( 4751 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4752 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4753 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4754 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4755 ) 4756 4757 else: 4758 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4759 4760 elif args.order: 4761 if 4 <= len(args.order) <= 7: 4762 trader.Order( 4763 operation=args.order[0], 4764 orderType=args.order[1], 4765 lots=int(args.order[2]), 4766 targetPrice=float(args.order[3]), 4767 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4768 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4769 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4770 ) 4771 4772 else: 4773 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4774 4775 elif args.buy_limit: 4776 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4777 4778 elif args.sell_limit: 4779 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4780 4781 elif args.buy_stop: 4782 if 2 <= len(args.buy_stop) <= 7: 4783 trader.BuyStop( 4784 lots=int(args.buy_stop[0]), 4785 targetPrice=float(args.buy_stop[1]), 4786 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4787 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4788 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4789 ) 4790 4791 else: 4792 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4793 4794 elif args.sell_stop: 4795 if 2 <= len(args.sell_stop) <= 7: 4796 trader.SellStop( 4797 lots=int(args.sell_stop[0]), 4798 targetPrice=float(args.sell_stop[1]), 4799 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4800 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4801 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4802 ) 4803 4804 else: 4805 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4806 4807 # elif args.buy_order_grid is not None: 4808 # # update order grid work with api v2 4809 # if len(args.buy_order_grid) == 2: 4810 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4811 # 4812 # for order in orderParams: 4813 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4814 # 4815 # else: 4816 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4817 # 4818 # elif args.sell_order_grid is not None: 4819 # # update order grid work with api v2 4820 # if len(args.sell_order_grid) >= 2: 4821 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4822 # 4823 # for order in orderParams: 4824 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4825 # 4826 # else: 4827 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4828 4829 elif args.close_order is not None: 4830 trader.CloseOrders(args.close_order) # close only one order 4831 4832 elif args.close_orders is not None: 4833 trader.CloseOrders(args.close_orders) # close list of orders 4834 4835 elif args.close_trade: 4836 if not (args.ticker or args.figi): 4837 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4838 raise Exception("Ticker or FIGI required") 4839 4840 if args.ticker: 4841 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4842 4843 else: 4844 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4845 4846 elif args.close_trades is not None: 4847 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4848 4849 elif args.close_all is not None: 4850 trader.CloseAll(*args.close_all) 4851 4852 elif args.limits: 4853 if args.output is not None: 4854 trader.withdrawalLimitsFile = args.output 4855 4856 trader.OverviewLimits(show=True) 4857 4858 elif args.user_info: 4859 if args.output is not None: 4860 trader.userInfoFile = args.output 4861 4862 trader.OverviewUserInfo(show=True) 4863 4864 elif args.account: 4865 if args.output is not None: 4866 trader.userAccountsFile = args.output 4867 4868 trader.OverviewAccounts(show=True) 4869 4870 else: 4871 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4872 raise Exception("There is no command to execute") 4873 4874 except Exception: 4875 trace = tb.format_exc() 4876 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4877 if e in trace: 4878 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4879 break 4880 4881 uLogger.debug(trace) 4882 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4883 exitCode = 255 # an error occurred, must be open a ticket for this issue 4884 4885 finally: 4886 finish = datetime.now(tzutc()) 4887 4888 if exitCode == 0: 4889 if args.more: 4890 uLogger.debug("All operations were finished success (summary code is 0).") 4891 4892 else: 4893 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4894 os.path.abspath(uLog.defaultLogFile), exitCode, 4895 )) 4896 4897 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4898 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4899 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4900 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4901 )) 4902 uLogger.debug("=-" * 50) 4903 4904 if not kwargs: 4905 sys.exit(exitCode) 4906 4907 else: 4908 return exitCode 4909 4910 4911if __name__ == "__main__": 4912 Main()
80def NanoToFloat(units: str, nano: int) -> float: 81 """ 82 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 83 84 `NanoToFloat(units="2", nano=500000000) -> 2.5` 85 86 `NanoToFloat(units="0", nano=50000000) -> 0.05` 87 88 :param units: integer string or integer parameter that represents the integer part of number 89 :param nano: integer string or integer parameter that represents the fractional part of number 90 :return: float view of number 91 """ 92 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
95def FloatToNano(number: float) -> dict: 96 """ 97 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 98 99 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 100 101 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 102 103 :param number: float number 104 :return: nano-type view of number: `{"units": "string", "nano": integer}` 105 """ 106 splitByPoint = str(number).split(".") 107 frac = 0 108 109 if len(splitByPoint) > 1: 110 if len(splitByPoint[1]) <= 9: 111 frac = int("{}{}".format( 112 int(splitByPoint[1]), 113 "0" * (9 - len(splitByPoint[1])), 114 )) 115 116 if (number < 0) and (frac > 0): 117 frac = -frac 118 119 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
122def GetDatesAsString(start: str = None, end: str = None) -> tuple: 123 """ 124 Create tuple of date and time strings with timezone parsed from user-friendly date. 125 126 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 127 128 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 129 An error exception will occur if input date has incorrect format. 130 131 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 132 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 133 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 134 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 135 136 Also, you can use keywords for start if `end=None`: 137 `today` (from 00:00:00 to the end of current day), 138 `yesterday` (-1 day from 00:00:00 to 23:59:59), 139 `week` (-7 day from 00:00:00 to the end of current day), 140 `month` (-30 day from 00:00:00 to the end of current day), 141 `year` (-365 day from 00:00:00 to the end of current day), 142 143 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 144 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 145 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 146 """ 147 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 148 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 149 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 150 151 # time between start and the end of the current day: 152 if start is None or start.lower() == "today": 153 pass 154 155 # from start of the last day to the end of the last day: 156 elif start.lower() == "yesterday": 157 s -= timedelta(days=1) 158 e -= timedelta(days=1) 159 160 # week (-7 day from 00:00:00 to the end of the current day): 161 elif start.lower() == "week": 162 s -= timedelta(days=6) # +1 current day already taken into account 163 164 # month (-30 day from 00:00:00 to the end of current day): 165 elif start.lower() == "month": 166 s -= timedelta(days=29) # +1 current day already taken into account 167 168 # year (-365 day from 00:00:00 to the end of current day): 169 elif start.lower() == "year": 170 s -= timedelta(days=364) # +1 current day already taken into account 171 172 # -N days ago to the end of current day: 173 elif start.startswith('-') and start[1:].isdigit(): 174 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 175 176 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 177 else: 178 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 179 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 180 181 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 182 s = s.strftime(TKS_DATE_TIME_FORMAT) 183 e = e.strftime(TKS_DATE_TIME_FORMAT) 184 185 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 186 187 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
190class TinkoffBrokerServer: 191 """ 192 This class implements methods to work with Tinkoff broker server. 193 194 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 195 196 About `token`: https://tinkoff.github.io/investAPI/token/ 197 """ 198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.moreDebug = False 301 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 302 303 self.historyFile = None 304 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 305 306 See also: `History()`. 307 """ 308 309 self.htmlHistoryFile = "index.html" 310 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 311 312 See also: `ShowHistoryChart()`. 313 """ 314 315 self.instrumentsFile = "instruments.md" 316 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 317 318 See also: `ShowInstrumentsInfo()`. 319 """ 320 321 self.searchResultsFile = "search-results.md" 322 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 323 324 See also: `SearchInstruments()`. 325 """ 326 327 self.pricesFile = "prices.md" 328 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 329 330 See also: `GetListOfPrices()`. 331 """ 332 333 self.infoFile = "info.md" 334 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 335 336 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 337 """ 338 339 self.bondsXLSXFile = "ext-bonds.xlsx" 340 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 341 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 342 343 See also: `ExtendBondsData()`. 344 """ 345 346 self.calendarFile = "calendar.md" 347 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 348 349 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 350 351 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 352 """ 353 354 self.overviewFile = "overview.md" 355 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 356 357 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 358 """ 359 360 self.overviewDigestFile = "overview-digest.md" 361 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 362 363 See also: `Overview()` with parameter `details="digest"`. 364 """ 365 366 self.overviewPositionsFile = "overview-positions.md" 367 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 368 369 See also: `Overview()` with parameter `details="positions"`. 370 """ 371 372 self.overviewOrdersFile = "overview-orders.md" 373 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 374 375 See also: `Overview()` with parameter `details="orders"`. 376 """ 377 378 self.overviewAnalyticsFile = "overview-analytics.md" 379 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 380 381 See also: `Overview()` with parameter `details="analytics"`. 382 """ 383 384 self.reportFile = "deals.md" 385 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 386 387 See also: `Deals()`. 388 """ 389 390 self.withdrawalLimitsFile = "limits.md" 391 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 392 393 See also: `OverviewLimits()` and `RequestLimits()`. 394 """ 395 396 self.userInfoFile = "user-info.md" 397 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 398 399 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 400 """ 401 402 self.userAccountsFile = "accounts.md" 403 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 404 405 See also: `OverviewAccounts()`, `RequestAccounts()`. 406 """ 407 408 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 409 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 410 411 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 412 413 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 414 """ 415 416 self.iList = None # init iList for raw instruments data 417 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 418 419 See also: `Listing()`, `DumpInstruments()`. 420 """ 421 422 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 423 if useCache: 424 if os.path.exists(self.iListDumpFile): 425 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 426 curTime = datetime.now(tzutc()) 427 428 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 429 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 430 431 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 432 433 else: 434 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 435 436 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 437 os.path.abspath(self.iListDumpFile), 438 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 439 )) 440 441 else: 442 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 443 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 444 445 else: 446 self.iList = self.Listing() # request new raw instruments data from broker server 447 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 448 449 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 450 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 451 452 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 453 """ 454 455 def _ParseJSON(self, rawData="{}") -> dict: 456 """ 457 Parse JSON from response string. 458 459 :param rawData: this is a string with JSON-formatted text. 460 :return: JSON (dictionary), parsed from server response string. 461 """ 462 responseJSON = json.loads(rawData) if rawData else {} 463 464 if self.moreDebug: 465 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 466 467 return responseJSON 468 469 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 470 """ 471 Send GET or POST request to broker server and receive JSON object. 472 473 self.header: must be defining with dictionary of headers. 474 self.body: if define then used as request body. None by default. 475 self.timeout: global request timeout, 15 seconds by default. 476 :param url: url with REST request. 477 :param reqType: send "GET" or "POST" request. "GET" by default. 478 :param retry: how many times retry after first request if an 5xx server errors occurred. 479 :param pause: sleep time in seconds between retries. 480 :return: response JSON (dictionary) from broker. 481 """ 482 if reqType not in ("GET", "POST"): 483 uLogger.error("You can define request type: 'GET' or 'POST'!") 484 raise Exception("Incorrect value") 485 486 if self.moreDebug: 487 uLogger.debug("Request parameters:") 488 uLogger.debug(" - REST API URL: {}".format(url)) 489 uLogger.debug(" - request type: {}".format(reqType)) 490 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 491 uLogger.debug(" - body:\n{}".format(self.body)) 492 493 # fast hack to avoid all operations with some tickers/FIGI 494 responseJSON = {} 495 oK = True 496 for item in self.exclude: 497 if item in url: 498 if self.moreDebug: 499 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 500 501 oK = False 502 break 503 504 if oK: 505 counter = 0 506 response = None 507 errMsg = "" 508 509 while not response and counter <= retry: 510 if reqType == "GET": 511 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 512 513 if reqType == "POST": 514 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 515 516 if self.moreDebug: 517 uLogger.debug("Response:") 518 uLogger.debug(" - status code: {}".format(response.status_code)) 519 uLogger.debug(" - reason: {}".format(response.reason)) 520 uLogger.debug(" - body length: {}".format(len(response.text))) 521 uLogger.debug(" - headers:\n{}".format(response.headers)) 522 523 # Server returns some headers: 524 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 525 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 526 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 527 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 528 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 529 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 530 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 531 sleep(rateLimitWait) 532 533 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 534 if 400 <= response.status_code < 500: 535 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 536 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 537 counter = retry + 1 538 539 if 500 <= response.status_code < 600: 540 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 541 uLogger.debug(" - not oK, {}".format(errMsg)) 542 counter += 1 543 544 if counter <= retry: 545 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 546 sleep(pause) 547 548 responseJSON = self._ParseJSON(rawData=response.text) 549 550 if errMsg: 551 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 552 uLogger.error(" - not oK, {}".format(errMsg)) 553 554 return responseJSON 555 556 def _IUpdater(self, iType: str) -> tuple: 557 """ 558 Request instrument by type from server. See available API methods for instruments: 559 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 560 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 561 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 562 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 563 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 564 565 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 566 :return: tuple with iType name and list of available instruments of current type for defined user token. 567 """ 568 result = [] 569 570 if iType in TKS_INSTRUMENTS: 571 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 572 573 # all instruments have the same body in API v2 requests: 574 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 575 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 576 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 577 578 return iType, result 579 580 def _IWrapper(self, kwargs): 581 """ 582 Wrapper runs instrument's update method `_IUpdater()`. 583 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 584 """ 585 return self._IUpdater(**kwargs) 586 587 def Listing(self) -> dict: 588 """ 589 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 590 591 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 592 """ 593 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 594 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 595 596 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 597 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 598 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 599 600 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 601 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 602 poolUpdater.close() 603 604 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 605 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 606 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 607 608 # calculate minimum price increment (step) for all instruments and set up instrument's type: 609 for iType in iList.keys(): 610 for ticker in iList[iType]: 611 iList[iType][ticker]["type"] = iType 612 613 if "minPriceIncrement" in iList[iType][ticker].keys(): 614 iList[iType][ticker]["step"] = NanoToFloat( 615 iList[iType][ticker]["minPriceIncrement"]["units"], 616 iList[iType][ticker]["minPriceIncrement"]["nano"], 617 ) 618 619 else: 620 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 621 622 return iList 623 624 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 625 """ 626 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 627 628 See also: `DumpInstruments()`, `Listing()`. 629 630 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 631 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 632 """ 633 if self.iListDumpFile is None or not self.iListDumpFile: 634 uLogger.error("Output name of dump file must be defined!") 635 raise Exception("Filename required") 636 637 if not self.iList or forceUpdate: 638 self.iList = self.Listing() 639 640 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 641 642 # Save as XLSX with separated sheets for every type of instruments: 643 with pd.ExcelWriter( 644 path=xlsxDumpFile, 645 date_format=TKS_DATE_FORMAT, 646 datetime_format=TKS_DATE_TIME_FORMAT, 647 mode="w", 648 ) as writer: 649 for iType in TKS_INSTRUMENTS: 650 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 651 df = df[sorted(df)] # sorted by column names 652 df = df.applymap( 653 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 654 na_action="ignore", 655 ) # converting numbers from nano-type to float in every cell 656 df.to_excel( 657 writer, 658 sheet_name=iType, 659 encoding="UTF-8", 660 freeze_panes=(1, 1), 661 ) # saving as XLSX-file with freeze first row and column as headers 662 663 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 664 665 def DumpInstruments(self, forceUpdate: bool = True) -> str: 666 """ 667 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 668 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 669 670 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 671 672 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 673 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 674 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 675 """ 676 if self.iListDumpFile is None or not self.iListDumpFile: 677 uLogger.error("Output name of dump file must be defined!") 678 raise Exception("Filename required") 679 680 if not self.iList or forceUpdate: 681 self.iList = self.Listing() 682 683 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 684 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 685 fH.write(jsonDump) 686 687 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 688 689 return jsonDump 690 691 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 692 """ 693 Show information about one instrument defined by json data and prints it in Markdown format. 694 695 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 696 697 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 698 :param show: if `True` then also printing information about instrument and its current price. 699 :return: multilines text in Markdown format with information about one instrument. 700 """ 701 splitLine = "| | |\n" 702 infoText = "" 703 704 if iJSON is not None and iJSON and isinstance(iJSON, dict): 705 info = [ 706 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 707 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 708 "| Parameters | Values |\n", 709 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 710 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 711 "| Full name: | {:<54} |\n".format(iJSON["name"]), 712 ] 713 714 if "sector" in iJSON.keys() and iJSON["sector"]: 715 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 716 717 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 718 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 719 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 720 ))) 721 722 info.extend([ 723 splitLine, 724 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 725 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 726 ]) 727 728 if "isin" in iJSON.keys() and iJSON["isin"]: 729 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 730 731 if "classCode" in iJSON.keys(): 732 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 733 734 info.extend([ 735 splitLine, 736 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 737 splitLine, 738 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 739 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 740 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 741 ]) 742 743 if iJSON["figi"]: 744 self.figi = iJSON["figi"] 745 iJSON = iJSON | self.RequestTradingStatus() 746 747 info.extend([ 748 splitLine, 749 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 750 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 751 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 752 ]) 753 754 info.append(splitLine) 755 756 if "type" in iJSON.keys() and iJSON["type"]: 757 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 758 759 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 760 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 761 762 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 763 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 764 765 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 766 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 767 768 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 769 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 770 771 if "focusType" in iJSON.keys() and iJSON["focusType"]: 772 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 773 774 if "assetType" in iJSON.keys() and iJSON["assetType"]: 775 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 776 777 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 778 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 779 780 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 781 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 782 783 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 784 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 785 786 if "currency" in iJSON.keys(): 787 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 788 789 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 790 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 791 792 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 793 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 794 795 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 796 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 797 798 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 799 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 800 801 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 802 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 803 804 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 805 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 806 807 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 808 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 809 810 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 811 info.append("| Perpetual bond: | Yes |\n") 812 813 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 814 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 815 816 iExt = None 817 if iJSON["type"] == "Bonds": 818 info.extend([ 819 splitLine, 820 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 821 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 822 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 823 iJSON["nominal"]["currency"], 824 )), 825 ]) 826 827 if "floatingCouponFlag" in iJSON.keys(): 828 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 829 830 if "amortizationFlag" in iJSON.keys(): 831 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 832 833 info.append(splitLine) 834 835 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 836 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 837 838 if iJSON["figi"]: 839 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 840 841 info.extend([ 842 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 843 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 844 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 845 ]) 846 847 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 848 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 849 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 850 iJSON["aciValue"]["currency"] 851 ))) 852 853 if "currentPrice" in iJSON.keys(): 854 info.append(splitLine) 855 856 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 857 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 858 859 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 860 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 861 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 862 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 863 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 864 865 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 866 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 867 868 info.extend([ 869 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 870 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 871 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 872 )), 873 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 874 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 875 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 876 )), 877 "| Changes between last deal price and last close | {:<54} |\n".format( 878 "{:.2f}%{}".format( 879 iJSON["currentPrice"]["changes"], 880 " ({}{:.2f} {})".format( 881 "+" if bondChangesDelta > 0 else "", 882 bondChangesDelta, 883 aciCurrency 884 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 885 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 886 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 887 currency 888 ), 889 ) 890 ), 891 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 892 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 893 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 894 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 897 )), 898 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 899 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 903 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 904 )), 905 ]) 906 907 if "lot" in iJSON.keys(): 908 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 909 910 if "step" in iJSON.keys() and iJSON["step"] != 0: 911 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 912 913 # Add bond payment calendar: 914 if iJSON["type"] == "Bonds": 915 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 916 info.extend(["\n", strCalendar]) 917 918 infoText += "".join(info) 919 920 if show: 921 uLogger.info("{}".format(infoText)) 922 923 else: 924 uLogger.debug("{}".format(infoText)) 925 926 if self.infoFile is not None: 927 with open(self.infoFile, "w", encoding="UTF-8") as fH: 928 fH.write(infoText) 929 930 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 931 932 return infoText 933 934 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 935 """ 936 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 937 938 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 939 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 940 :return: JSON formatted data with information about instrument. 941 """ 942 tickerJSON = {} 943 if self.moreDebug: 944 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 945 946 if not self.ticker: 947 uLogger.warning("self.ticker variable is not be empty!") 948 949 else: 950 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 951 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 952 raise Exception("Instrument not allowed") 953 954 if not self.iList: 955 self.iList = self.Listing() 956 957 if self.ticker in self.iList["Shares"].keys(): 958 tickerJSON = self.iList["Shares"][self.ticker] 959 if self.moreDebug: 960 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 961 962 elif self.ticker in self.iList["Currencies"].keys(): 963 tickerJSON = self.iList["Currencies"][self.ticker] 964 if self.moreDebug: 965 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Bonds"].keys(): 968 tickerJSON = self.iList["Bonds"][self.ticker] 969 if self.moreDebug: 970 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Etfs"].keys(): 973 tickerJSON = self.iList["Etfs"][self.ticker] 974 if self.moreDebug: 975 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Futures"].keys(): 978 tickerJSON = self.iList["Futures"][self.ticker] 979 if self.moreDebug: 980 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 981 982 if tickerJSON: 983 self.figi = tickerJSON["figi"] 984 985 if requestPrice: 986 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 987 988 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 989 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 990 991 else: 992 tickerJSON["currentPrice"]["changes"] = 0 993 994 if show: 995 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 996 997 else: 998 if show: 999 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1000 1001 return tickerJSON 1002 1003 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1004 """ 1005 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1006 1007 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1008 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1009 :return: JSON formatted data with information about instrument. 1010 """ 1011 figiJSON = {} 1012 if self.moreDebug: 1013 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1014 1015 if not self.figi: 1016 uLogger.warning("self.figi variable is not be empty!") 1017 1018 else: 1019 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1020 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1021 raise Exception("Instrument not allowed") 1022 1023 if not self.iList: 1024 self.iList = self.Listing() 1025 1026 for item in self.iList["Shares"].keys(): 1027 if self.figi == self.iList["Shares"][item]["figi"]: 1028 figiJSON = self.iList["Shares"][item] 1029 1030 if self.moreDebug: 1031 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1032 1033 break 1034 1035 if not figiJSON: 1036 for item in self.iList["Currencies"].keys(): 1037 if self.figi == self.iList["Currencies"][item]["figi"]: 1038 figiJSON = self.iList["Currencies"][item] 1039 1040 if self.moreDebug: 1041 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1042 1043 break 1044 1045 if not figiJSON: 1046 for item in self.iList["Bonds"].keys(): 1047 if self.figi == self.iList["Bonds"][item]["figi"]: 1048 figiJSON = self.iList["Bonds"][item] 1049 1050 if self.moreDebug: 1051 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1052 1053 break 1054 1055 if not figiJSON: 1056 for item in self.iList["Etfs"].keys(): 1057 if self.figi == self.iList["Etfs"][item]["figi"]: 1058 figiJSON = self.iList["Etfs"][item] 1059 1060 if self.moreDebug: 1061 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1062 1063 break 1064 1065 if not figiJSON: 1066 for item in self.iList["Futures"].keys(): 1067 if self.figi == self.iList["Futures"][item]["figi"]: 1068 figiJSON = self.iList["Futures"][item] 1069 1070 if self.moreDebug: 1071 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1072 1073 break 1074 1075 if figiJSON: 1076 self.figi = figiJSON["figi"] 1077 self.ticker = figiJSON["ticker"] 1078 1079 if requestPrice: 1080 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1081 1082 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1083 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1084 1085 else: 1086 figiJSON["currentPrice"]["changes"] = 0 1087 1088 if show: 1089 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1090 1091 else: 1092 if show: 1093 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1094 1095 return figiJSON 1096 1097 def GetCurrentPrices(self, show: bool = True) -> dict: 1098 """ 1099 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1100 `{"buy": [{"price": 1243.8, "quantity": 193}, 1101 {"price": 1244.0, "quantity": 168}, 1102 {"price": 1244.8, "quantity": 5}, 1103 {"price": 1245.0, "quantity": 61}, 1104 {"price": 1245.4, "quantity": 60}], 1105 "sell": [{"price": 1243.6, "quantity": 8}, 1106 {"price": 1242.6, "quantity": 10}, 1107 {"price": 1242.4, "quantity": 18}, 1108 {"price": 1242.2, "quantity": 50}, 1109 {"price": 1242.0, "quantity": 113}], 1110 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1111 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1112 - sell: list of dicts with Buyers prices, 1113 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1114 - quantity: volume value by current price in lots, 1115 - limitUp: current trade session limit price, maximum, 1116 - limitDown: current trade session limit price, minimum, 1117 - lastPrice: last deal price of the instrument, 1118 - closePrice: previous trade session close price of the instrument. 1119 1120 See also: `SearchByTicker()` and `SearchByFIGI()`. 1121 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1122 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1123 1124 :param show: if `True` then print DOM to log and console. 1125 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1126 If an error occurred then returns an empty record: 1127 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1128 """ 1129 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1130 1131 if self.depth < 1: 1132 uLogger.error("Depth of Market (DOM) must be >=1!") 1133 raise Exception("Incorrect value") 1134 1135 if not (self.ticker or self.figi): 1136 uLogger.error("self.ticker or self.figi variables must be defined!") 1137 raise Exception("Ticker or FIGI required") 1138 1139 if self.ticker and not self.figi: 1140 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1141 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1142 1143 if not self.ticker and self.figi: 1144 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1145 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1146 1147 if not self.figi: 1148 uLogger.error("FIGI is not defined!") 1149 raise Exception("Ticker or FIGI required") 1150 1151 else: 1152 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1153 1154 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1155 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1156 self.body = str({"figi": self.figi, "depth": self.depth}) 1157 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1158 1159 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1160 # list of dicts with sellers orders: 1161 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1162 1163 # list of dicts with buyers orders: 1164 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1165 1166 # max price of instrument at this time: 1167 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1168 1169 # min price of instrument at this time: 1170 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1171 1172 # last price of deal with instrument: 1173 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1174 1175 # last close price of instrument: 1176 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1177 1178 else: 1179 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1180 uLogger.debug("Server response: {}".format(pricesResponse)) 1181 1182 if show: 1183 if prices["buy"] or prices["sell"]: 1184 info = [ 1185 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1186 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1187 self.ticker, 1188 self.figi, 1189 self.depth, 1190 ), 1191 "-" * 60, "\n", 1192 " Orders of Buyers | Orders of Sellers\n", 1193 "-" * 60, "\n", 1194 " Sell prices (volumes) | Buy prices (volumes)\n", 1195 "-" * 60, "\n", 1196 ] 1197 1198 if not prices["buy"]: 1199 info.append(" | No orders!\n") 1200 sumBuy = 0 1201 1202 else: 1203 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1204 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1205 for item in maxMinSorted: 1206 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1207 1208 if not prices["sell"]: 1209 info.append("No orders! |\n") 1210 sumSell = 0 1211 1212 else: 1213 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1214 for item in prices["sell"]: 1215 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1216 1217 info.extend([ 1218 "-" * 60, "\n", 1219 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1220 "-" * 60, "\n", 1221 ]) 1222 1223 infoText = "".join(info) 1224 1225 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1226 1227 else: 1228 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1229 1230 return prices 1231 1232 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1233 """ 1234 This method get and show information about all available broker instruments for current user account. 1235 If `instrumentsFile` string is not empty then also save information to this file. 1236 1237 :param show: if `True` then print results to console, if `False` - print only to file. 1238 :return: multi-lines string with all available broker instruments 1239 """ 1240 if not self.iList: 1241 self.iList = self.Listing() 1242 1243 info = [ 1244 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1245 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1246 ] 1247 1248 # add instruments count by type: 1249 for iType in self.iList.keys(): 1250 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1251 1252 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1253 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1254 1255 # generating info tables with all instruments by type: 1256 for iType in self.iList.keys(): 1257 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1258 1259 for instrument in self.iList[iType].keys(): 1260 iName = self.iList[iType][instrument]["name"] # instrument's name 1261 if len(iName) > 57: 1262 iName = "{}...".format(iName[:54]) # right trim for a long string 1263 1264 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1265 self.iList[iType][instrument]["ticker"], 1266 iName, 1267 self.iList[iType][instrument]["figi"], 1268 self.iList[iType][instrument]["currency"], 1269 self.iList[iType][instrument]["lot"], 1270 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1271 )) 1272 1273 infoText = "".join(info) 1274 1275 if show: 1276 uLogger.info(infoText) 1277 1278 if self.instrumentsFile: 1279 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1280 fH.write(infoText) 1281 1282 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1283 1284 return infoText 1285 1286 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1287 """ 1288 This method search and show information about instruments by part of its ticker, FIGI or name. 1289 If `searchResultsFile` string is not empty then also save information to this file. 1290 1291 :param pattern: string with part of ticker, FIGI or instrument's name. 1292 :param show: if `True` then print results to console, if `False` - return list of result only. 1293 :return: list of dictionaries with all found instruments. 1294 """ 1295 if not self.iList: 1296 self.iList = self.Listing() 1297 1298 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1299 compiledPattern = re.compile(pattern, re.IGNORECASE) 1300 1301 for iType in self.iList: 1302 for instrument in self.iList[iType].values(): 1303 searchResult = compiledPattern.search(" ".join( 1304 [instrument["ticker"], instrument["figi"], instrument["name"]] 1305 )) 1306 1307 if searchResult: 1308 searchResults[iType][instrument["ticker"]] = instrument 1309 1310 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1311 info = [ 1312 "# Search results\n\n", 1313 "* **Search pattern:** [{}]\n".format(pattern), 1314 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1315 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1316 ] 1317 infoShort = info[:] 1318 1319 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1320 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1321 skippedLine = "| ... | ... | ... | ... |\n" 1322 1323 if resultsLen == 0: 1324 info.append("\nNo results\n") 1325 infoShort.append("\nNo results\n") 1326 uLogger.warning("No results. Try changing your search pattern.") 1327 1328 else: 1329 for iType in searchResults: 1330 iTypeValuesCount = len(searchResults[iType].values()) 1331 if iTypeValuesCount > 0: 1332 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1333 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1334 1335 for instrument in searchResults[iType].values(): 1336 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1337 instrument["type"], 1338 instrument["ticker"], 1339 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1340 instrument["figi"], 1341 )) 1342 1343 if iTypeValuesCount <= 5: 1344 infoShort.extend(info[-iTypeValuesCount:]) 1345 1346 else: 1347 infoShort.extend(info[-5:]) 1348 infoShort.append(skippedLine) 1349 1350 infoText = "".join(info) 1351 infoTextShort = "".join(infoShort) 1352 1353 if show: 1354 uLogger.info(infoTextShort) 1355 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1356 1357 if self.searchResultsFile: 1358 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1359 fH.write(infoText) 1360 1361 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1362 1363 return searchResults 1364 1365 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1366 """ 1367 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1368 1369 :param instruments: list of strings with tickers or FIGIs. 1370 :return: list with unique instrument FIGIs only. 1371 """ 1372 requestedInstruments = [] 1373 for iName in instruments: 1374 if iName not in self.aliases.keys(): 1375 if iName not in requestedInstruments: 1376 requestedInstruments.append(iName) 1377 1378 else: 1379 if iName not in requestedInstruments: 1380 if self.aliases[iName] not in requestedInstruments: 1381 requestedInstruments.append(self.aliases[iName]) 1382 1383 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1384 1385 onlyUniqueFIGIs = [] 1386 for iName in requestedInstruments: 1387 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1388 continue 1389 1390 self.ticker = iName 1391 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1392 1393 if not iData: 1394 self.ticker = "" 1395 self.figi = iName 1396 1397 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1398 1399 if not iData: 1400 self.figi = "" 1401 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1402 1403 if iData and iData["figi"] not in onlyUniqueFIGIs: 1404 onlyUniqueFIGIs.append(iData["figi"]) 1405 1406 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1407 1408 return onlyUniqueFIGIs 1409 1410 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1411 """ 1412 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1413 See limits: https://tinkoff.github.io/investAPI/limits/ 1414 If `pricesFile` string is not empty then also save information to this file. 1415 1416 :param instruments: list of strings with tickers or FIGIs. 1417 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1418 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1419 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1420 """ 1421 if instruments is None or not instruments: 1422 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1423 raise Exception("Ticker or FIGI required") 1424 1425 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1426 1427 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1428 1429 iList = [] # trying to get info and current prices about all unique instruments: 1430 for self.figi in onlyUniqueFIGIs: 1431 iData = self.SearchByFIGI(requestPrice=True) 1432 iList.append(iData) 1433 1434 self.ShowListOfPrices(iList, show) 1435 1436 return iList 1437 1438 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1439 """ 1440 Show table contains current prices of given instruments. 1441 1442 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1443 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1444 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1445 :return: multilines text in Markdown format as a table contains current prices. 1446 """ 1447 infoText = "" 1448 1449 if show or self.pricesFile: 1450 info = [ 1451 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1452 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1453 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1454 ] 1455 1456 for item in iList: 1457 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1458 item["ticker"], 1459 item["figi"], 1460 item["type"], 1461 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1462 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1463 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1464 "{} / {}".format( 1465 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1466 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1467 ), 1468 "{} / {}".format( 1469 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1470 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1471 ), 1472 item["currency"], 1473 )) 1474 1475 infoText = "".join(info) 1476 1477 if show: 1478 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1479 1480 if self.pricesFile: 1481 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1482 fH.write(infoText) 1483 1484 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1485 1486 return infoText 1487 1488 def RequestTradingStatus(self) -> dict: 1489 """ 1490 Requesting trading status for the instrument defined by `figi` variable. 1491 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1492 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1493 1494 :return: dictionary with trading status attributes. Response example: 1495 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1496 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1497 """ 1498 if self.figi is None or not self.figi: 1499 uLogger.error("Variable `figi` must be defined for using this method!") 1500 raise Exception("FIGI required") 1501 1502 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1503 1504 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1505 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1506 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1507 1508 if self.moreDebug: 1509 uLogger.debug("Records about current trading status successfully received") 1510 1511 return tradingStatus 1512 1513 def RequestPortfolio(self) -> dict: 1514 """ 1515 Requesting actual user's portfolio for current `accountId`. 1516 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1517 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1518 1519 :return: dictionary with user's portfolio. 1520 """ 1521 if self.accountId is None or not self.accountId: 1522 uLogger.error("Variable `accountId` must be defined for using this method!") 1523 raise Exception("Account ID required") 1524 1525 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1526 1527 self.body = str({"accountId": self.accountId}) 1528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1529 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1530 1531 if self.moreDebug: 1532 uLogger.debug("Records about user's portfolio successfully received") 1533 1534 return rawPortfolio 1535 1536 def RequestPositions(self) -> dict: 1537 """ 1538 Requesting open positions by currencies and instruments for current `accountId`. 1539 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1540 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1541 1542 :return: dictionary with open positions by instruments. 1543 """ 1544 if self.accountId is None or not self.accountId: 1545 uLogger.error("Variable `accountId` must be defined for using this method!") 1546 raise Exception("Account ID required") 1547 1548 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1549 1550 self.body = str({"accountId": self.accountId}) 1551 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1552 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1553 1554 if self.moreDebug: 1555 uLogger.debug("Records about current open positions successfully received") 1556 1557 return rawPositions 1558 1559 def RequestPendingOrders(self) -> list: 1560 """ 1561 Requesting current actual pending orders for current `accountId`. 1562 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1563 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1564 1565 :return: list of dictionaries with pending orders. 1566 """ 1567 if self.accountId is None or not self.accountId: 1568 uLogger.error("Variable `accountId` must be defined for using this method!") 1569 raise Exception("Account ID required") 1570 1571 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1572 1573 self.body = str({"accountId": self.accountId}) 1574 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1575 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1576 1577 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1578 1579 return rawOrders 1580 1581 def RequestStopOrders(self) -> list: 1582 """ 1583 Requesting current actual stop orders for current `accountId`. 1584 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1585 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1586 1587 :return: list of dictionaries with stop orders. 1588 """ 1589 if self.accountId is None or not self.accountId: 1590 uLogger.error("Variable `accountId` must be defined for using this method!") 1591 raise Exception("Account ID required") 1592 1593 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1594 1595 self.body = str({"accountId": self.accountId}) 1596 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1597 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1598 1599 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1600 1601 return rawStopOrders 1602 1603 def Overview(self, show: bool = False, details: str = "full") -> dict: 1604 """ 1605 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1606 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1607 are defined then also save information to file. 1608 1609 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1610 many requests about the state of the portfolio, and then, based on the received data, a large number 1611 of calculation and statistics are collected. 1612 1613 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1614 :param details: how detailed should the information be? You should specify one of strings: 1615 `full` - shows full available information about portfolio status (by default), 1616 `positions` - shows only open positions, 1617 `digest` - show a short digest of the portfolio status, 1618 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1619 `orders` - shows only sections of open limits and stop orders. 1620 :return: dictionary with client's raw portfolio and some statistics. 1621 """ 1622 if self.accountId is None or not self.accountId: 1623 uLogger.error("Variable `accountId` must be defined for using this method!") 1624 raise Exception("Account ID required") 1625 1626 view = { 1627 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1628 "headers": {}, # list of dictionaries, response headers without "positions" section 1629 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1630 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1631 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1632 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1633 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1634 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1635 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1636 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1637 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1638 }, 1639 "stat": { # --- some statistics calculated using "raw" sections: 1640 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1641 "availableRUB": 0., # available rubles (without other currencies) 1642 "blockedRUB": 0., # blocked sum in Russian Rouble 1643 "totalChangesRUB": 0., # changes for all open trades in RUB 1644 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1645 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1646 "sharesCostRUB": 0., # costs of all shares in RUB 1647 "bondsCostRUB": 0., # costs of all bonds in RUB 1648 "etfsCostRUB": 0., # costs of all etfs in RUB 1649 "futuresCostRUB": 0., # costs of all futures in RUB 1650 "Currencies": [], # list of dictionaries of all currencies statistics 1651 "Shares": [], # list of dictionaries of all shares statistics 1652 "Bonds": [], # list of dictionaries of all bonds statistics 1653 "Etfs": [], # list of dictionaries of all etfs statistics 1654 "Futures": [], # list of dictionaries of all futures statistics 1655 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1656 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1657 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1658 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1659 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1660 }, 1661 "analytics": { # --- some analytics of portfolio: 1662 "distrByAssets": {}, # portfolio distribution by assets 1663 "distrByCompanies": {}, # portfolio distribution by companies 1664 "distrBySectors": {}, # portfolio distribution by sectors 1665 "distrByCurrencies": {}, # portfolio distribution by currencies 1666 "distrByCountries": {}, # portfolio distribution by countries 1667 } 1668 } 1669 1670 details = details.lower() 1671 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1672 if details not in availableDetails: 1673 details = "full" 1674 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1675 1676 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1677 1678 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1679 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1680 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1681 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1682 1683 # save response headers without "positions" section: 1684 for key in portfolioResponse.keys(): 1685 if key != "positions": 1686 view["raw"]["headers"][key] = portfolioResponse[key] 1687 1688 else: 1689 continue 1690 1691 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1692 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1693 for item in portfolioResponse["positions"]: 1694 if item["instrumentType"] == "currency": 1695 self.figi = item["figi"] 1696 curr = self.SearchByFIGI(requestPrice=False) 1697 1698 # current price of currency in RUB: 1699 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1700 "name": curr["name"], 1701 "currentPrice": NanoToFloat( 1702 item["currentPrice"]["units"], 1703 item["currentPrice"]["nano"] 1704 ), 1705 } 1706 1707 view["raw"]["Currencies"].append(item) 1708 1709 elif item["instrumentType"] == "share": 1710 view["raw"]["Shares"].append(item) 1711 1712 elif item["instrumentType"] == "bond": 1713 view["raw"]["Bonds"].append(item) 1714 1715 elif item["instrumentType"] == "etf": 1716 view["raw"]["Etfs"].append(item) 1717 1718 elif item["instrumentType"] == "futures": 1719 view["raw"]["Futures"].append(item) 1720 1721 else: 1722 continue 1723 1724 # how many volume of currencies (by ISO currency name) are blocked: 1725 for item in view["raw"]["positions"]["blocked"]: 1726 blocked = NanoToFloat(item["units"], item["nano"]) 1727 if blocked > 0: 1728 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1729 1730 # how many volume of instruments (by FIGI) are blocked: 1731 for item in view["raw"]["positions"]["securities"]: 1732 blocked = int(item["blocked"]) 1733 if blocked > 0: 1734 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1735 1736 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1737 1738 if "rub" in allBlocked.keys(): 1739 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1740 1741 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1742 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1743 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1744 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1745 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1746 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1747 view["stat"]["portfolioCostRUB"] = sum([ 1748 view["stat"]["allCurrenciesCostRUB"], 1749 view["stat"]["sharesCostRUB"], 1750 view["stat"]["bondsCostRUB"], 1751 view["stat"]["etfsCostRUB"], 1752 view["stat"]["futuresCostRUB"], 1753 ]) 1754 1755 # --- calculating some portfolio statistics: 1756 byComp = {} # distribution by companies 1757 bySect = {} # distribution by sectors 1758 byCurr = {} # distribution by currencies (include RUB) 1759 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1760 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1761 1762 for item in portfolioResponse["positions"]: 1763 self.figi = item["figi"] 1764 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1765 1766 if instrument: 1767 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1768 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1769 1770 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1771 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1772 1773 else: 1774 blocked = 0 1775 1776 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1777 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1778 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1779 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1780 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1781 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1782 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1783 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1784 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1785 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1786 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1787 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1788 1789 statData = { 1790 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1791 "ticker": instrument["ticker"], # ticker by FIGI 1792 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1793 "volume": volume, # available volume of instrument 1794 "lots": lots, # volume in lots of instrument 1795 "direction": direction, # direction of an instrument's position: short or long 1796 "blocked": blocked, # blocked volume of currency or instrument 1797 "currentPrice": curPrice, # current instrument's price in basic asset 1798 "average": average, # current average position price 1799 "cost": cost, # current cost of all volume of instrument in basic asset 1800 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1801 "costRUB": costRUB, # cost of instrument in ruble 1802 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1803 "profit": profit, # expected profit at current moment 1804 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1805 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1806 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1807 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1808 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1809 "step": instrument["step"], # minimum price increment 1810 } 1811 1812 # adding distribution by unique countries: 1813 if statData["country"] not in byCountry.keys(): 1814 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1815 1816 else: 1817 byCountry[statData["country"]]["cost"] += costRUB 1818 byCountry[statData["country"]]["percent"] += percentCostRUB 1819 1820 if item["instrumentType"] != "currency": 1821 # adding distribution by unique companies: 1822 if statData["name"]: 1823 if statData["name"] not in byComp.keys(): 1824 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1825 1826 else: 1827 byComp[statData["name"]]["cost"] += costRUB 1828 byComp[statData["name"]]["percent"] += percentCostRUB 1829 1830 # adding distribution by unique sectors: 1831 if statData["sector"] not in bySect.keys(): 1832 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1833 1834 else: 1835 bySect[statData["sector"]]["cost"] += costRUB 1836 bySect[statData["sector"]]["percent"] += percentCostRUB 1837 1838 # adding distribution by unique currencies: 1839 if currency not in byCurr.keys(): 1840 byCurr[currency] = { 1841 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1842 "cost": costRUB, 1843 "percent": percentCostRUB 1844 } 1845 1846 else: 1847 byCurr[currency]["cost"] += costRUB 1848 byCurr[currency]["percent"] += percentCostRUB 1849 1850 # saving statistics for every instrument: 1851 if item["instrumentType"] == "currency": 1852 view["stat"]["Currencies"].append(statData) 1853 1854 # update dict with free funds for trading (total - blocked) by currencies 1855 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1856 view["stat"]["funds"][currency] = { 1857 "total": volume, 1858 "totalCostRUB": costRUB, # total volume cost in rubles 1859 "free": volume - blocked, 1860 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1861 } 1862 1863 elif item["instrumentType"] == "share": 1864 view["stat"]["Shares"].append(statData) 1865 1866 elif item["instrumentType"] == "bond": 1867 view["stat"]["Bonds"].append(statData) 1868 1869 elif item["instrumentType"] == "etf": 1870 view["stat"]["Etfs"].append(statData) 1871 1872 elif item["instrumentType"] == "Futures": 1873 view["stat"]["Futures"].append(statData) 1874 1875 else: 1876 continue 1877 1878 # total changes in Russian Ruble: 1879 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1880 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1881 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1882 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1883 view["stat"]["funds"]["rub"] = { 1884 "total": view["stat"]["availableRUB"], 1885 "totalCostRUB": view["stat"]["availableRUB"], 1886 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1887 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1888 } 1889 1890 # --- pending orders sector data: 1891 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1892 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1893 1894 for item in view["raw"]["orders"]: 1895 self.figi = item["figi"] 1896 1897 if item["figi"] not in uniquePendingOrdersFIGIs: 1898 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1899 1900 uniquePendingOrdersFIGIs.append(item["figi"]) 1901 uniquePendingOrders[item["figi"]] = instrument 1902 1903 else: 1904 instrument = uniquePendingOrders[item["figi"]] 1905 1906 if instrument: 1907 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1908 orderType = TKS_ORDER_TYPES[item["orderType"]] 1909 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1910 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1911 1912 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1913 if item["direction"] == "ORDER_DIRECTION_BUY": 1914 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1915 1916 else: 1917 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1918 1919 # requested price for order execution: 1920 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1921 1922 # necessary changes in percent to reach target from current price: 1923 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1924 1925 view["stat"]["orders"].append({ 1926 "orderID": item["orderId"], # orderId number parameter of current order 1927 "figi": item["figi"], # FIGI identification 1928 "ticker": instrument["ticker"], # ticker name by FIGI 1929 "lotsRequested": item["lotsRequested"], # requested lots value 1930 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1931 "currentPrice": lastPrice, # current instrument's price for defined action 1932 "targetPrice": target, # requested price for order execution in base currency 1933 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1934 "percentChanges": changes, # changes in percent to target from current price 1935 "currency": item["currency"], # instrument's currency name 1936 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1937 "type": orderType, # type of order from TKS_ORDER_TYPES 1938 "status": orderState, # order status from TKS_ORDER_STATES 1939 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1940 }) 1941 1942 # --- stop orders sector data: 1943 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1944 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1945 1946 for item in view["raw"]["stopOrders"]: 1947 self.figi = item["figi"] 1948 1949 if item["figi"] not in uniqueStopOrdersFIGIs: 1950 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1951 1952 uniqueStopOrdersFIGIs.append(item["figi"]) 1953 uniqueStopOrders[item["figi"]] = instrument 1954 1955 else: 1956 instrument = uniqueStopOrders[item["figi"]] 1957 1958 if instrument: 1959 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1960 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1961 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1962 1963 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1964 if "expirationTime" in item.keys(): 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1966 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1967 1968 else: 1969 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1970 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1971 1972 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1973 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1974 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1975 1976 else: 1977 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1978 1979 # requested price when stop-order executed: 1980 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1981 1982 # price for limit-order, set up when stop-order executed: 1983 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1984 1985 # necessary changes in percent to reach target from current price: 1986 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1987 1988 view["stat"]["stopOrders"].append({ 1989 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1990 "figi": item["figi"], # FIGI identification 1991 "ticker": instrument["ticker"], # ticker name by FIGI 1992 "lotsRequested": item["lotsRequested"], # requested lots value 1993 "currentPrice": lastPrice, # current instrument's price for defined action 1994 "targetPrice": target, # requested price for stop-order execution in base currency 1995 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1996 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1997 "percentChanges": changes, # changes in percent to target from current price 1998 "currency": item["currency"], # instrument's currency name 1999 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2000 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2001 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2002 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2003 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2004 }) 2005 2006 # --- calculating data for analytics section: 2007 # portfolio distribution by assets: 2008 view["analytics"]["distrByAssets"] = { 2009 "Ruble": { 2010 "uniques": 1, 2011 "cost": view["stat"]["availableRUB"], 2012 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2013 }, 2014 "Currencies": { 2015 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2016 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2017 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 }, 2019 "Shares": { 2020 "uniques": len(view["stat"]["Shares"]), 2021 "cost": view["stat"]["sharesCostRUB"], 2022 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2023 }, 2024 "Bonds": { 2025 "uniques": len(view["stat"]["Bonds"]), 2026 "cost": view["stat"]["bondsCostRUB"], 2027 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2028 }, 2029 "Etfs": { 2030 "uniques": len(view["stat"]["Etfs"]), 2031 "cost": view["stat"]["etfsCostRUB"], 2032 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 "Futures": { 2035 "uniques": len(view["stat"]["Futures"]), 2036 "cost": view["stat"]["futuresCostRUB"], 2037 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 } 2040 2041 # portfolio distribution by companies: 2042 view["analytics"]["distrByCompanies"]["All money cash"] = { 2043 "ticker": "", 2044 "cost": view["stat"]["allCurrenciesCostRUB"], 2045 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2046 } 2047 view["analytics"]["distrByCompanies"].update(byComp) 2048 2049 # portfolio distribution by sectors: 2050 view["analytics"]["distrBySectors"]["All money cash"] = { 2051 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2052 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2053 } 2054 view["analytics"]["distrBySectors"].update(bySect) 2055 2056 # portfolio distribution by currencies: 2057 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2058 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2059 2060 if self.moreDebug: 2061 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2062 2063 view["analytics"]["distrByCurrencies"].update(byCurr) 2064 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2065 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2066 2067 # portfolio distribution by countries: 2068 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2069 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2070 2071 if self.moreDebug: 2072 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2073 2074 view["analytics"]["distrByCountries"].update(byCountry) 2075 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2076 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2077 2078 # --- Prepare text statistics overview in human-readable: 2079 if show: 2080 # Whatever the value `details`, header not changes: 2081 info = [ 2082 "# Client's portfolio\n\n", 2083 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2084 "* **Account ID:** [{}]\n".format(self.accountId), 2085 ] 2086 2087 if details in ["full", "positions", "digest"]: 2088 info.extend([ 2089 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2090 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2091 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2092 view["stat"]["totalChangesRUB"], 2093 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2094 view["stat"]["totalChangesPercentRUB"], 2095 ), 2096 ]) 2097 2098 if details in ["full", "positions"]: 2099 info.extend([ 2100 "## Open positions\n\n", 2101 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2102 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2103 "| Ruble | {:>31} | | | | | |\n".format( 2104 "{:.2f} ({:.2f}) rub".format( 2105 view["stat"]["availableRUB"], 2106 view["stat"]["blockedRUB"], 2107 ) 2108 ) 2109 ]) 2110 2111 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2112 return [ 2113 "| | | | | | | |\n", 2114 "| {:<27} | | | | | {:>19} | |\n".format( 2115 noTradeStr if noTradeStr else typeStr, 2116 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2117 ), 2118 ] 2119 2120 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2121 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2122 "{} [{}]".format(data["ticker"], data["figi"]), 2123 "{:.2f} ({:.2f}) {}".format( 2124 data["volume"], 2125 data["blocked"], 2126 data["currency"], 2127 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2128 data["volume"], 2129 data["blocked"], 2130 ), 2131 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2132 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2133 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2134 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2135 "{}{:.2f} {} ({}{:.2f}%)".format( 2136 "+" if data["profit"] > 0 else "", 2137 data["profit"], data["baseCurrencyName"], 2138 "+" if data["percentProfit"] > 0 else "", 2139 data["percentProfit"], 2140 ), 2141 ) 2142 2143 # --- Show currencies section: 2144 if view["stat"]["Currencies"]: 2145 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2146 for item in view["stat"]["Currencies"]: 2147 info.append(_InfoStr(item, showCurrencyName=True)) 2148 2149 else: 2150 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2151 2152 # --- Show shares section: 2153 if view["stat"]["Shares"]: 2154 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2155 2156 for item in view["stat"]["Shares"]: 2157 info.append(_InfoStr(item)) 2158 2159 else: 2160 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2161 2162 # --- Show bonds section: 2163 if view["stat"]["Bonds"]: 2164 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2165 2166 for item in view["stat"]["Bonds"]: 2167 info.append(_InfoStr(item)) 2168 2169 else: 2170 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2171 2172 # --- Show etfs section: 2173 if view["stat"]["Etfs"]: 2174 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2175 2176 for item in view["stat"]["Etfs"]: 2177 info.append(_InfoStr(item)) 2178 2179 else: 2180 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2181 2182 # --- Show futures section: 2183 if view["stat"]["Futures"]: 2184 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2185 2186 for item in view["stat"]["Futures"]: 2187 info.append(_InfoStr(item)) 2188 2189 else: 2190 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2191 2192 if details in ["full", "orders"]: 2193 # --- Show pending orders section: 2194 if view["stat"]["orders"]: 2195 info.extend([ 2196 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2197 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2198 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2199 ]) 2200 2201 for item in view["stat"]["orders"]: 2202 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2203 "{} [{}]".format(item["ticker"], item["figi"]), 2204 item["orderID"], 2205 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2206 "{} {} ({}{:.2f}%)".format( 2207 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2208 item["baseCurrencyName"], 2209 "+" if item["percentChanges"] > 0 else "", 2210 float(item["percentChanges"]), 2211 ), 2212 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2213 item["action"], 2214 item["type"], 2215 item["date"], 2216 )) 2217 2218 else: 2219 info.append("\n## Total pending limit-orders: 0\n") 2220 2221 # --- Show stop orders section: 2222 if view["stat"]["stopOrders"]: 2223 info.extend([ 2224 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2225 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2226 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2227 ]) 2228 2229 for item in view["stat"]["stopOrders"]: 2230 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2231 "{} [{}]".format(item["ticker"], item["figi"]), 2232 item["orderID"], 2233 item["lotsRequested"], 2234 "{} {} ({}{:.2f}%)".format( 2235 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2236 item["baseCurrencyName"], 2237 "+" if item["percentChanges"] > 0 else "", 2238 float(item["percentChanges"]), 2239 ), 2240 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2241 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2242 item["action"], 2243 item["type"], 2244 item["expType"], 2245 item["createDate"], 2246 item["expDate"], 2247 )) 2248 2249 else: 2250 info.append("\n## Total stop-orders: 0\n") 2251 2252 if details in ["full", "analytics"]: 2253 # -- Show analytics section: 2254 if view["stat"]["portfolioCostRUB"] > 0: 2255 info.extend([ 2256 "\n# Analytics\n" 2257 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2258 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2259 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2260 view["stat"]["totalChangesRUB"], 2261 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2262 view["stat"]["totalChangesPercentRUB"], 2263 ), 2264 "\n## Portfolio distribution by assets\n" 2265 "\n| Type | Uniques | Percent | Current cost |\n", 2266 "|------------|---------|---------|--------------------|\n", 2267 ]) 2268 2269 for key in view["analytics"]["distrByAssets"].keys(): 2270 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2271 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2272 key, 2273 view["analytics"]["distrByAssets"][key]["uniques"], 2274 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2275 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2276 )) 2277 2278 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2279 info.extend([ 2280 "\n## Portfolio distribution by companies\n" 2281 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2282 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2283 ]) 2284 2285 for company in view["analytics"]["distrByCompanies"].keys(): 2286 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2287 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2288 info.append("| {} | {:<7} | {:<18} |\n".format( 2289 "{}{}{}".format( 2290 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2291 company, 2292 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2293 ), 2294 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2295 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2296 )) 2297 2298 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2299 info.extend([ 2300 "\n## Portfolio distribution by sectors\n" 2301 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2302 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2303 ]) 2304 2305 for sector in view["analytics"]["distrBySectors"].keys(): 2306 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2307 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2308 sector, 2309 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2310 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2311 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2312 )) 2313 2314 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2315 info.extend([ 2316 "\n## Portfolio distribution by currencies\n" 2317 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2318 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2319 ]) 2320 2321 for curr in view["analytics"]["distrByCurrencies"].keys(): 2322 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2323 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2324 info.append("| {} | {:<7} | {:<18} |\n".format( 2325 "[{}] {}{}".format( 2326 curr, 2327 view["analytics"]["distrByCurrencies"][curr]["name"], 2328 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2329 ), 2330 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2331 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2332 )) 2333 2334 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2335 info.extend([ 2336 "\n## Portfolio distribution by countries\n" 2337 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2338 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2339 ]) 2340 2341 for country in view["analytics"]["distrByCountries"].keys(): 2342 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2343 nameLen = len(country) 2344 info.append("| {} | {:<7} | {:<18} |\n".format( 2345 "{}{}".format( 2346 country, 2347 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2348 ), 2349 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2350 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2351 )) 2352 2353 infoText = "".join(info) 2354 2355 uLogger.info(infoText) 2356 2357 if details == "full" and self.overviewFile: 2358 filename = self.overviewFile 2359 2360 elif details == "digest" and self.overviewDigestFile: 2361 filename = self.overviewDigestFile 2362 2363 elif details == "positions" and self.overviewPositionsFile: 2364 filename = self.overviewPositionsFile 2365 2366 elif details == "orders" and self.overviewOrdersFile: 2367 filename = self.overviewOrdersFile 2368 2369 elif details == "analytics" and self.overviewAnalyticsFile: 2370 filename = self.overviewAnalyticsFile 2371 2372 else: 2373 filename = "" 2374 2375 if filename: 2376 with open(filename, "w", encoding="UTF-8") as fH: 2377 fH.write(infoText) 2378 2379 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2380 2381 return view 2382 2383 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2384 """ 2385 Returns history operations between two given dates for current `accountId`. 2386 If `reportFile` string is not empty then also save human-readable report. 2387 Shows some statistical data of closed positions. 2388 2389 :param start: see docstring in `GetDatesAsString()` method 2390 :param end: see docstring in `GetDatesAsString()` method 2391 :param show: if `True` then also prints all records to the console. 2392 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2393 :return: original list of dictionaries with history of deals records from API ("operations" key): 2394 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2395 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2396 """ 2397 if self.accountId is None or not self.accountId: 2398 uLogger.error("Variable `accountId` must be defined for using this method!") 2399 raise Exception("Account ID required") 2400 2401 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2402 2403 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2404 2405 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2406 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2407 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2408 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2409 customStat = {} # custom statistics in additional to responseJSON 2410 2411 # --- output report in human-readable format: 2412 if show or self.reportFile: 2413 splitLine1 = "| | | | | |\n" # Summary section 2414 splitLine2 = "| | | | | | | | |\n" # Operations section 2415 nextDay = "" 2416 2417 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2418 2419 if len(ops) > 0: 2420 customStat = { 2421 "opsCount": 0, # total operations count 2422 "buyCount": 0, # buy operations 2423 "sellCount": 0, # sell operations 2424 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2425 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2426 "payIn": {"rub": 0.}, # Deposit brokerage account 2427 "payOut": {"rub": 0.}, # Withdrawals 2428 "divs": {"rub": 0.}, # Dividends income 2429 "coupons": {"rub": 0.}, # Coupon's income 2430 "brokerCom": {"rub": 0.}, # Service commissions 2431 "serviceCom": {"rub": 0.}, # Service commissions 2432 "marginCom": {"rub": 0.}, # Margin commissions 2433 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2434 } 2435 2436 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2437 for item in ops: 2438 if item["state"] == "OPERATION_STATE_EXECUTED": 2439 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2440 2441 # count buy operations: 2442 if "_BUY" in item["operationType"]: 2443 customStat["buyCount"] += 1 2444 2445 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2446 customStat["buyTotal"][item["payment"]["currency"]] += payment 2447 2448 else: 2449 customStat["buyTotal"][item["payment"]["currency"]] = payment 2450 2451 # count sell operations: 2452 elif "_SELL" in item["operationType"]: 2453 customStat["sellCount"] += 1 2454 2455 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2456 customStat["sellTotal"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["sellTotal"][item["payment"]["currency"]] = payment 2460 2461 # count incoming operations: 2462 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2463 if item["payment"]["currency"] in customStat["payIn"].keys(): 2464 customStat["payIn"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["payIn"][item["payment"]["currency"]] = payment 2468 2469 # count withdrawals operations: 2470 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2471 if item["payment"]["currency"] in customStat["payOut"].keys(): 2472 customStat["payOut"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["payOut"][item["payment"]["currency"]] = payment 2476 2477 # count dividends income: 2478 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2479 if item["payment"]["currency"] in customStat["divs"].keys(): 2480 customStat["divs"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["divs"][item["payment"]["currency"]] = payment 2484 2485 # count coupon's income: 2486 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2487 if item["payment"]["currency"] in customStat["coupons"].keys(): 2488 customStat["coupons"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["coupons"][item["payment"]["currency"]] = payment 2492 2493 # count broker commissions: 2494 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2495 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2496 customStat["brokerCom"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["brokerCom"][item["payment"]["currency"]] = payment 2500 2501 # count service commissions: 2502 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2503 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2504 customStat["serviceCom"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["serviceCom"][item["payment"]["currency"]] = payment 2508 2509 # count margin commissions: 2510 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2511 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2512 customStat["marginCom"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["marginCom"][item["payment"]["currency"]] = payment 2516 2517 # count withholding taxes: 2518 elif "_TAX" in item["operationType"]: 2519 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2520 customStat["allTaxes"][item["payment"]["currency"]] += payment 2521 2522 else: 2523 customStat["allTaxes"][item["payment"]["currency"]] = payment 2524 2525 else: 2526 continue 2527 2528 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2529 2530 # --- view "Actions" lines: 2531 info.extend([ 2532 "| Report sections | | | | |\n", 2533 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2534 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2535 "| | Buy: {:<22} | {:<28} | | |\n".format( 2536 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2537 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2538 ), 2539 "| | Sell: {:<21} | {:<28} | | |\n".format( 2540 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2541 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2542 ), 2543 ]) 2544 2545 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2546 for key in opsKeys: 2547 if key == "rub": 2548 continue 2549 2550 info.extend([ 2551 "| | | {:<28} | | |\n".format( 2552 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2553 ), 2554 "| | | {:<28} | | |\n".format( 2555 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2556 ), 2557 ]) 2558 2559 info.append(splitLine1) 2560 2561 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2562 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2563 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2564 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2565 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2566 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2567 ) 2568 2569 # --- view "Payments" lines: 2570 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2571 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2572 2573 for key in paymentsKeys: 2574 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2575 2576 info.append(splitLine1) 2577 2578 # --- view "Commissions and taxes" lines: 2579 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2580 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2581 2582 for key in comKeys: 2583 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2584 2585 info.append(splitLine1) 2586 2587 info.extend([ 2588 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2589 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2590 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2591 ]) 2592 2593 else: 2594 info.append("Broker returned no operations during this period\n") 2595 2596 # --- view "Operations" section: 2597 for item in ops: 2598 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2599 continue 2600 2601 else: 2602 self.figi = item["figi"] if item["figi"] else "" 2603 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2604 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2605 2606 # group of deals during one day: 2607 if nextDay and item["date"].split("T")[0] != nextDay: 2608 info.append(splitLine2) 2609 nextDay = "" 2610 2611 else: 2612 nextDay = item["date"].split("T")[0] # saving current day for splitting 2613 2614 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2615 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2616 self.figi if self.figi else "—", 2617 instrument["ticker"] if instrument else "—", 2618 instrument["type"] if instrument else "—", 2619 item["quantity"] if int(item["quantity"]) > 0 else "—", 2620 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2621 TKS_OPERATION_STATES[item["state"]], 2622 TKS_OPERATION_TYPES[item["operationType"]], 2623 )) 2624 2625 infoText = "".join(info) 2626 2627 if show: 2628 if self.moreDebug: 2629 uLogger.debug("Records about history of a client's operations successfully received") 2630 2631 uLogger.info(infoText) 2632 2633 if self.reportFile: 2634 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2635 fH.write(infoText) 2636 2637 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2638 2639 return ops, customStat 2640 2641 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2642 """ 2643 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2644 2645 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2646 Warning! Broker server used ISO UTC time by default. 2647 2648 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2649 Also, `historyFile` used to update history with `onlyMissing` parameter. 2650 2651 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2652 2653 :param start: see docstring in `GetDatesAsString()` method. 2654 :param end: see docstring in `GetDatesAsString()` method. 2655 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2656 `"hour"`, `"day"`. Default: `"hour"`. 2657 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2658 False by default. Warning! History appends only from last candle to current time 2659 with always update last candle! 2660 :param csvSep: separator if csv-file is used, `,` by default. 2661 :param show: if `True` then also prints Pandas DataFrame to the console. 2662 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2663 `["date", "time", "open", "high", "low", "close", "volume"]`. 2664 """ 2665 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2666 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2667 history = None # empty pandas object for history 2668 2669 if interval not in TKS_CANDLE_INTERVALS.keys(): 2670 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2671 raise Exception("Incorrect value") 2672 2673 if not (self.ticker or self.figi): 2674 uLogger.error("Ticker or FIGI must be defined!") 2675 raise Exception("Ticker or FIGI required") 2676 2677 if self.ticker and not self.figi: 2678 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2679 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2680 2681 if self.figi and not self.ticker: 2682 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2683 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2684 2685 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2686 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2687 if interval.lower() != "day": 2688 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2689 2690 delta = dtEnd - dtStart # current UTC time minus last time in file 2691 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2692 2693 # calculate history length in candles: 2694 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2695 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2696 length += 1 # to avoid fraction time 2697 2698 # calculate data blocks count: 2699 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2700 2701 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2702 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2703 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2704 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2705 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2706 2707 tempOld = None # pandas object for old history, if --only-missing key present 2708 lastTime = None # datetime object of last old candle in file 2709 2710 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2711 uLogger.debug("--only-missing key present, add only last missing candles...") 2712 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2713 2714 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2715 2716 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2717 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2718 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2719 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2720 2721 # get last datetime object from last string in file or minus 1 delta if file is empty: 2722 if len(tempOld) > 0: 2723 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2724 2725 else: 2726 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2727 2728 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2729 2730 responseJSONs = [] # raw history blocks of data 2731 2732 blockEnd = dtEnd 2733 for item in range(blocks): 2734 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2735 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2736 2737 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2738 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2739 )) 2740 2741 if blockStart == blockEnd: 2742 uLogger.debug("Skipped this zero-length block...") 2743 2744 else: 2745 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2746 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2747 self.body = str({ 2748 "figi": self.figi, 2749 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2750 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2751 "interval": TKS_CANDLE_INTERVALS[interval][0] 2752 }) 2753 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2754 2755 if "code" in responseJSON.keys(): 2756 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2757 2758 else: 2759 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2760 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2761 2762 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2763 2764 blockEnd = blockStart 2765 2766 printCount = len(responseJSONs) # candles to show in console 2767 if responseJSONs: 2768 tempHistory = pd.DataFrame( 2769 data={ 2770 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2771 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2772 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2773 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2774 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2775 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2776 "volume": [int(item["volume"]) for item in responseJSONs], 2777 }, 2778 index=range(len(responseJSONs)), 2779 columns=["date", "time", "open", "high", "low", "close", "volume"], 2780 ) 2781 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2782 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2783 2784 # append only newest candles to old history if --only-missing key present: 2785 if onlyMissing and tempOld is not None and lastTime is not None: 2786 index = 0 # find start index in tempHistory data: 2787 2788 for i, item in tempHistory.iterrows(): 2789 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2790 2791 if curTime == lastTime: 2792 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2793 index = i 2794 printCount = index + 1 2795 break 2796 2797 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2798 2799 else: 2800 history = tempHistory # if no `--only-missing` key then load full data from server 2801 2802 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2803 2804 if history is not None and not history.empty: 2805 if show: 2806 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2807 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2808 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2809 )) 2810 2811 else: 2812 uLogger.warning("Received an empty candles history!") 2813 2814 if self.historyFile is not None: 2815 if history is not None and not history.empty: 2816 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2817 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2818 2819 else: 2820 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2821 2822 else: 2823 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2824 2825 return history 2826 2827 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2828 """ 2829 Load candles history from csv-file and return Pandas DataFrame object. 2830 2831 See also: `History()` and `ShowHistoryChart()` methods. 2832 2833 :param filePath: path to csv-file to open. 2834 """ 2835 loadedHistory = None # init candles data object 2836 2837 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2838 2839 if os.path.exists(filePath): 2840 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2841 2842 tfStr = self.priceModel.FormattedDelta( 2843 self.priceModel.timeframe, 2844 "{days} days {hours}h {minutes}m {seconds}s", 2845 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2846 self.priceModel.timeframe, 2847 "{hours}h {minutes}m {seconds}s", 2848 ) 2849 2850 if loadedHistory is not None and not loadedHistory.empty: 2851 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2852 len(loadedHistory), 2853 tfStr, 2854 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2855 ) 2856 2857 else: 2858 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2859 2860 else: 2861 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2862 2863 return loadedHistory 2864 2865 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2866 """ 2867 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2868 2869 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2870 Default: `index.html` (both for interact and non-interact candlesticks chart). 2871 2872 See also: `History()` and `LoadHistory()` methods. 2873 2874 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2875 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2876 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2877 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2878 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2879 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2880 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2881 """ 2882 if isinstance(candles, str): 2883 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2884 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2885 2886 elif isinstance(candles, pd.DataFrame): 2887 self.priceModel.prices = candles # set candles chain from variable 2888 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2889 2890 if "datetime" not in candles.columns: 2891 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2892 2893 else: 2894 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2895 raise Exception("Incorrect value") 2896 2897 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2898 2899 if interact: 2900 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2901 2902 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2903 2904 else: 2905 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2906 2907 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2908 2909 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2910 2911 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2912 """ 2913 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2914 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2915 2916 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2917 2918 :param operation: string "Buy" or "Sell". 2919 :param lots: volume, integer count of lots >= 1. 2920 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2921 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2922 :param expDate: string "Undefined" by default or local date in future, 2923 it is a string with format `%Y-%m-%d %H:%M:%S`. 2924 :return: JSON with response from broker server. 2925 """ 2926 if self.accountId is None or not self.accountId: 2927 uLogger.error("Variable `accountId` must be defined for using this method!") 2928 raise Exception("Account ID required") 2929 2930 if operation is None or not operation or operation not in ("Buy", "Sell"): 2931 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2932 raise Exception("Incorrect value") 2933 2934 if lots is None or lots < 1: 2935 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2936 lots = 1 2937 2938 if tp is None or tp < 0: 2939 tp = 0 2940 2941 if sl is None or sl < 0: 2942 sl = 0 2943 2944 if expDate is None or not expDate: 2945 expDate = "Undefined" 2946 2947 if not (self.ticker or self.figi): 2948 uLogger.error("Ticker or FIGI must be defined!") 2949 raise Exception("Ticker or FIGI required") 2950 2951 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2952 self.ticker = instrument["ticker"] 2953 self.figi = instrument["figi"] 2954 2955 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2956 2957 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2958 self.body = str({ 2959 "figi": self.figi, 2960 "quantity": str(lots), 2961 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2962 "accountId": str(self.accountId), 2963 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2964 }) 2965 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2966 2967 if "orderId" in response.keys(): 2968 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2969 operation, response["orderId"], 2970 self.ticker, self.figi, lots, 2971 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2972 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2973 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2974 )) 2975 2976 if tp > 0: 2977 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2978 2979 if sl > 0: 2980 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2981 2982 else: 2983 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2984 2985 return response 2986 2987 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2988 """ 2989 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2990 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2991 2992 See also: `Order()` and `Trade()` docstrings. 2993 2994 :param lots: volume, integer count of lots >= 1. 2995 :param tp: float > 0, take profit price of stop-order. 2996 :param sl: float > 0, stop loss price of stop-order. 2997 :param expDate: it's a local date in future. 2998 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2999 :return: JSON with response from broker server. 3000 """ 3001 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3002 3003 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3004 """ 3005 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3006 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3007 3008 See also: `Order()` and `Trade()` docstrings. 3009 3010 :param lots: volume, integer count of lots >= 1. 3011 :param tp: float > 0, take profit price of stop-order. 3012 :param sl: float > 0, stop loss price of stop-order. 3013 :param expDate: it's a local date in the future. 3014 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3015 :return: JSON with response from broker server. 3016 """ 3017 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3018 3019 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3020 """ 3021 Close position of given instruments. 3022 3023 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3024 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3025 This avoids unnecessary downloading data from the server. 3026 """ 3027 if instruments is None or not instruments: 3028 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3029 raise Exception("Ticker or FIGI required") 3030 3031 if isinstance(instruments, str): 3032 instruments = [instruments] 3033 3034 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3035 if uniqueInstruments: 3036 if portfolio is None or not portfolio: 3037 portfolio = self.Overview(show=False) 3038 3039 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3040 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3041 3042 for self.figi in uniqueInstruments: 3043 if self.figi not in allOpened: 3044 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3045 continue 3046 3047 # search open trade info about instrument by ticker: 3048 instrument = {} 3049 for iType in TKS_INSTRUMENTS: 3050 if instrument: 3051 break 3052 3053 for item in portfolio["stat"][iType]: 3054 if item["figi"] == self.figi: 3055 instrument = item 3056 break 3057 3058 if instrument: 3059 self.ticker = instrument["ticker"] 3060 self.figi = instrument["figi"] 3061 3062 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3063 self.ticker, 3064 self.figi, 3065 int(instrument["volume"]), 3066 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3067 )) 3068 3069 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3070 3071 if tradeLots > 0: 3072 if instrument["blocked"] > 0: 3073 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3074 instrument["blocked"], 3075 self.ticker, 3076 tradeLots, 3077 )) 3078 3079 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3080 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3081 3082 else: 3083 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3084 3085 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3086 """ 3087 Close all positions of given instruments with defined type. 3088 3089 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3090 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3091 This avoids unnecessary downloading data from the server. 3092 """ 3093 if iType not in TKS_INSTRUMENTS: 3094 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3095 3096 else: 3097 if portfolio is None or not portfolio: 3098 portfolio = self.Overview(show=False) 3099 3100 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3101 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3102 3103 if tickers and portfolio: 3104 self.CloseTrades(tickers, portfolio) 3105 3106 else: 3107 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3108 3109 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3110 """ 3111 Universal method to create market or limit orders with all available parameters for current `accountId`. 3112 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3113 3114 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3115 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3116 3117 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3118 then broker immediately open market order as you can do simple --buy or --sell operations! 3119 3120 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3121 When current price will go up or down to target price value then broker opens a limit order. 3122 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3123 3124 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3125 3126 :param operation: string "Buy" or "Sell". 3127 :param orderType: string "Limit" or "Stop". 3128 :param lots: volume, integer count of lots >= 1. 3129 :param targetPrice: target price > 0. This is open trade price for limit order. 3130 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3131 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3132 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3133 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3134 Stop loss order always executed by market price. 3135 :param expDate: string "Undefined" by default or local date in future. 3136 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3137 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3138 A limit order has no expiration date, it lasts until the end of the trading day. 3139 :return: JSON with response from broker server. 3140 """ 3141 if self.accountId is None or not self.accountId: 3142 uLogger.error("Variable `accountId` must be defined for using this method!") 3143 raise Exception("Account ID required") 3144 3145 if operation is None or not operation or operation not in ("Buy", "Sell"): 3146 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3147 raise Exception("Incorrect value") 3148 3149 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3150 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3151 raise Exception("Incorrect value") 3152 3153 if lots is None or lots < 1: 3154 uLogger.error("You must define trade volume > 0: integer count of lots!") 3155 raise Exception("Incorrect value") 3156 3157 if targetPrice is None or targetPrice <= 0: 3158 uLogger.error("Target price for limit-order must be greater than 0!") 3159 raise Exception("Incorrect value") 3160 3161 if limitPrice is None or limitPrice <= 0: 3162 limitPrice = targetPrice 3163 3164 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3165 stopType = "Limit" 3166 3167 if expDate is None or not expDate: 3168 expDate = "Undefined" 3169 3170 if not (self.ticker or self.figi): 3171 uLogger.error("Tocker or FIGI must be defined!") 3172 raise Exception("Ticker or FIGI required") 3173 3174 response = {} 3175 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3176 self.ticker = instrument["ticker"] 3177 self.figi = instrument["figi"] 3178 3179 if orderType == "Limit": 3180 uLogger.debug( 3181 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3182 self.ticker, self.figi, 3183 operation, lots, targetPrice, instrument["currency"], 3184 )) 3185 3186 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3187 self.body = str({ 3188 "figi": self.figi, 3189 "quantity": str(lots), 3190 "price": FloatToNano(targetPrice), 3191 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3192 "accountId": str(self.accountId), 3193 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3194 }) 3195 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3196 3197 if "orderId" in response.keys(): 3198 uLogger.info( 3199 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3200 response["orderId"], 3201 self.ticker, self.figi, 3202 operation, lots, targetPrice, instrument["currency"], 3203 )) 3204 3205 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3206 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3207 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3208 targetPrice, instrument["currency"], 3209 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3210 )) 3211 3212 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3213 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3214 targetPrice, instrument["currency"], 3215 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3216 )) 3217 3218 else: 3219 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3220 3221 if orderType == "Stop": 3222 uLogger.debug( 3223 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3224 self.ticker, self.figi, 3225 operation, lots, 3226 targetPrice, instrument["currency"], 3227 limitPrice, instrument["currency"], 3228 stopType, expDate, 3229 )) 3230 3231 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3232 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3233 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3234 3235 body = { 3236 "figi": self.figi, 3237 "quantity": str(lots), 3238 "price": FloatToNano(limitPrice), 3239 "stopPrice": FloatToNano(targetPrice), 3240 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3241 "accountId": str(self.accountId), 3242 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3243 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3244 } 3245 3246 if expDateUTC: 3247 body["expireDate"] = expDateUTC 3248 3249 self.body = str(body) 3250 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3251 3252 if "stopOrderId" in response.keys(): 3253 uLogger.info( 3254 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3255 response["stopOrderId"], 3256 self.ticker, self.figi, 3257 operation, lots, 3258 targetPrice, instrument["currency"], 3259 limitPrice, instrument["currency"], 3260 TKS_STOP_ORDER_TYPES[stopOrderType], 3261 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3262 )) 3263 3264 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3265 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3266 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3267 targetPrice, instrument["currency"], 3268 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3269 )) 3270 3271 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3272 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3273 targetPrice, instrument["currency"], 3274 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3275 )) 3276 3277 else: 3278 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3279 3280 return response 3281 3282 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3283 """ 3284 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3285 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3286 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3287 See also: `Order()` docstring. 3288 3289 :param lots: volume, integer count of lots >= 1. 3290 :param targetPrice: target price > 0. This is open trade price for limit order. 3291 :return: JSON with response from broker server. 3292 """ 3293 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3294 3295 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3296 """ 3297 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3298 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3299 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3300 target price value then broker opens a limit order. See also: `Order()` docstring. 3301 3302 :param lots: volume, integer count of lots >= 1. 3303 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3304 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3305 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3306 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3307 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3308 :param expDate: string "Undefined" by default or local date in future. 3309 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3310 This date is converting to UTC format for server. 3311 :return: JSON with response from broker server. 3312 """ 3313 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3314 3315 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3316 """ 3317 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3318 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3319 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3320 See also: `Order()` docstring. 3321 3322 :param lots: volume, integer count of lots >= 1. 3323 :param targetPrice: target price > 0. This is open trade price for limit order. 3324 :return: JSON with response from broker server. 3325 """ 3326 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3327 3328 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3329 """ 3330 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3331 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3332 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3333 target price value then broker opens a limit order. See also: `Order()` docstring. 3334 3335 :param lots: volume, integer count of lots >= 1. 3336 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3337 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3338 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3339 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3340 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3341 :param expDate: string "Undefined" by default or local date in future. 3342 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3343 This date is converting to UTC format for server. 3344 :return: JSON with response from broker server. 3345 """ 3346 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3347 3348 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3349 """ 3350 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3351 3352 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3353 :param allOrdersIDs: pre-received lists of all active pending orders. 3354 This avoids unnecessary downloading data from the server. 3355 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3356 """ 3357 if self.accountId is None or not self.accountId: 3358 uLogger.error("Variable `accountId` must be defined for using this method!") 3359 raise Exception("Account ID required") 3360 3361 if orderIDs: 3362 if allOrdersIDs is None or not allOrdersIDs: 3363 rawOrders = self.RequestPendingOrders() 3364 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3365 3366 if allStopOrdersIDs is None or not allStopOrdersIDs: 3367 rawStopOrders = self.RequestStopOrders() 3368 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3369 3370 for orderID in orderIDs: 3371 idInPendingOrders = orderID in allOrdersIDs 3372 idInStopOrders = orderID in allStopOrdersIDs 3373 3374 if not (idInPendingOrders or idInStopOrders): 3375 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3376 continue 3377 3378 else: 3379 if idInPendingOrders: 3380 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3381 3382 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3383 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3384 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3385 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3386 3387 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3388 if self.moreDebug: 3389 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3390 3391 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3392 3393 else: 3394 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3395 3396 elif idInStopOrders: 3397 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3398 3399 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3400 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3401 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3402 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3403 3404 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3405 if self.moreDebug: 3406 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3407 3408 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3409 3410 else: 3411 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3412 3413 else: 3414 continue 3415 3416 def CloseAllOrders(self) -> None: 3417 """ 3418 Gets a list of open pending and stop orders and cancel it all. 3419 """ 3420 rawOrders = self.RequestPendingOrders() 3421 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3422 lenOrders = len(allOrdersIDs) 3423 3424 rawStopOrders = self.RequestStopOrders() 3425 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3426 lenSOrders = len(allStopOrdersIDs) 3427 3428 if lenOrders > 0 or lenSOrders > 0: 3429 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3430 3431 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3432 3433 else: 3434 uLogger.info("Orders not found, nothing to cancel.") 3435 3436 def CloseAll(self, *args) -> None: 3437 """ 3438 Close all available (not blocked) opened trades and orders. 3439 3440 Also, you can select one or more keywords case-insensitive: 3441 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3442 3443 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3444 """ 3445 overview = self.Overview(show=False) # get all open trades info 3446 3447 if len(args) == 0: 3448 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3449 self.CloseAllOrders() # close all pending and stop orders 3450 3451 for iType in TKS_INSTRUMENTS: 3452 if iType != "Currencies": 3453 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3454 3455 else: 3456 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3457 lowerArgs = [x.lower() for x in args] 3458 3459 if "orders" in lowerArgs: 3460 self.CloseAllOrders() # close all pending and stop orders 3461 3462 for iType in TKS_INSTRUMENTS: 3463 if iType.lower() in lowerArgs and iType != "Currencies": 3464 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3465 3466 @staticmethod 3467 def ParseOrderParameters(operation, **inputParameters): 3468 """ 3469 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3470 3471 :param operation: string "Buy" or "Sell". 3472 :param inputParameters: this is dict of strings that looks like this 3473 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3474 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3475 "prices" key: one or more prices to open limit-orders 3476 Counts of values in lots and prices lists must be equals! 3477 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3478 """ 3479 # TODO: update order grid work with api v2 3480 pass 3481 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3482 # 3483 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3484 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3485 # raise Exception("Incorrect value") 3486 # 3487 # if "l" in inputParameters.keys(): 3488 # inputParameters["lots"] = inputParameters.pop("l") 3489 # 3490 # if "p" in inputParameters.keys(): 3491 # inputParameters["prices"] = inputParameters.pop("p") 3492 # 3493 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3494 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3495 # raise Exception("Incorrect value") 3496 # 3497 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3498 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3499 # 3500 # if len(lots) != len(prices): 3501 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3502 # raise Exception("Incorrect value") 3503 # 3504 # uLogger.debug("Extracted parameters for orders:") 3505 # uLogger.debug("lots = {}".format(lots)) 3506 # uLogger.debug("prices = {}".format(prices)) 3507 # 3508 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3509 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3510 # uLogger.debug("Order parameters: {}".format(result)) 3511 # 3512 # return result 3513 3514 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3515 """ 3516 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3517 3518 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3519 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3520 """ 3521 result = False 3522 msg = "Instrument not defined!" 3523 3524 if portfolio is None or not portfolio: 3525 portfolio = self.Overview(show=False) 3526 3527 if self.ticker: 3528 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3529 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3530 3531 for iType in TKS_INSTRUMENTS: 3532 for instrument in portfolio["stat"][iType]: 3533 if instrument["ticker"] == self.ticker: 3534 result = True 3535 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3536 break 3537 3538 elif self.figi: 3539 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3540 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3541 3542 for iType in TKS_INSTRUMENTS: 3543 for instrument in portfolio["stat"][iType]: 3544 if instrument["figi"] == self.figi: 3545 result = True 3546 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3547 break 3548 3549 else: 3550 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3551 3552 uLogger.debug(msg) 3553 3554 return result 3555 3556 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3557 """ 3558 Returns instrument is in the user's portfolio if it presents there. 3559 Instrument must be defined by `ticker` (highly priority) or `figi`. 3560 3561 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3562 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3563 """ 3564 result = None 3565 msg = "Instrument not defined!" 3566 3567 if portfolio is None or not portfolio: 3568 portfolio = self.Overview(show=False) 3569 3570 if self.ticker: 3571 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3572 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3573 3574 for iType in TKS_INSTRUMENTS: 3575 for instrument in portfolio["stat"][iType]: 3576 if instrument["ticker"] == self.ticker: 3577 result = instrument 3578 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3579 break 3580 3581 elif self.figi: 3582 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3583 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3584 3585 for iType in TKS_INSTRUMENTS: 3586 for instrument in portfolio["stat"][iType]: 3587 if instrument["figi"] == self.figi: 3588 result = instrument 3589 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3590 break 3591 3592 else: 3593 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3594 3595 uLogger.debug(msg) 3596 3597 return result 3598 3599 def RequestLimits(self) -> dict: 3600 """ 3601 Method for obtaining the available funds for withdrawal for current `accountId`. 3602 3603 See also: 3604 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3605 - `OverviewLimits()` method 3606 3607 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3608 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3609 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3610 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3611 """ 3612 if self.accountId is None or not self.accountId: 3613 uLogger.error("Variable `accountId` must be defined for using this method!") 3614 raise Exception("Account ID required") 3615 3616 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3617 3618 self.body = str({"accountId": self.accountId}) 3619 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3620 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3621 3622 if self.moreDebug: 3623 uLogger.debug("Records about available funds for withdrawal successfully received") 3624 3625 return rawLimits 3626 3627 def OverviewLimits(self, show: bool = False) -> dict: 3628 """ 3629 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3630 3631 See also: `RequestLimits()`. 3632 3633 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3634 :return: dict with raw parsed data from server and some calculated statistics about it. 3635 """ 3636 if self.accountId is None or not self.accountId: 3637 uLogger.error("Variable `accountId` must be defined for using this method!") 3638 raise Exception("Account ID required") 3639 3640 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3641 3642 view = { 3643 "rawLimits": rawLimits, 3644 "limits": { # parsed data for every currency: 3645 "money": { # this is an array of portfolio currency positions 3646 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3647 }, 3648 "blocked": { # this is an array of blocked currency 3649 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3650 }, 3651 "blockedGuarantee": { # this is locked money under collateral for futures 3652 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3653 }, 3654 }, 3655 } 3656 3657 # --- Prepare text table with limits in human-readable format: 3658 if show: 3659 info = [ 3660 "# Withdrawal limits\n\n", 3661 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3662 "* **Account ID:** [{}]\n".format(self.accountId), 3663 ] 3664 3665 if view["limits"]["money"]: 3666 info.extend([ 3667 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3668 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3669 ]) 3670 3671 else: 3672 info.append("\nNo withdrawal limits\n") 3673 3674 for curr in view["limits"]["money"].keys(): 3675 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3676 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3677 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3678 3679 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3680 "[{}]".format(curr), 3681 "{:.2f}".format(view["limits"]["money"][curr]), 3682 "{:.2f}".format(availableMoney), 3683 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3684 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3685 ) 3686 3687 if curr == "rub": 3688 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3689 3690 else: 3691 info.append(infoStr) 3692 3693 infoText = "".join(info) 3694 3695 uLogger.info(infoText) 3696 3697 if self.withdrawalLimitsFile: 3698 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3699 fH.write(infoText) 3700 3701 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3702 3703 return view 3704 3705 def RequestAccounts(self) -> dict: 3706 """ 3707 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3708 3709 See also: 3710 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3711 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3712 - `OverviewUserInfo()` method 3713 3714 :return: dict with raw data from server that contains accounts info. Example of dict: 3715 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3716 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3717 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3718 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3719 """ 3720 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3721 3722 self.body = str({}) 3723 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3724 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3725 3726 if self.moreDebug: 3727 uLogger.debug("Records about available accounts successfully received") 3728 3729 return rawAccounts 3730 3731 def RequestUserInfo(self) -> dict: 3732 """ 3733 Method for requesting common user's information. 3734 3735 See also: 3736 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3737 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3738 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3739 - `OverviewUserInfo()` method 3740 3741 :return: dict with raw data from server that contains user's information. Example of dict: 3742 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3743 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3744 """ 3745 uLogger.debug("Requesting common user's information. Wait, please...") 3746 3747 self.body = str({}) 3748 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3749 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3750 3751 if self.moreDebug: 3752 uLogger.debug("Records about current user successfully received") 3753 3754 return rawUserInfo 3755 3756 def RequestMarginStatus(self, accountId: str = None) -> dict: 3757 """ 3758 Method for requesting margin calculation for defined account ID. 3759 3760 See also: 3761 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3762 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3763 - `OverviewUserInfo()` method 3764 3765 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3766 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3767 Example of responses: 3768 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3769 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3770 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3771 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3772 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3773 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3774 """ 3775 if accountId is None or not accountId: 3776 if self.accountId is None or not self.accountId: 3777 uLogger.error("Variable `accountId` must be defined for using this method!") 3778 raise Exception("Account ID required") 3779 3780 else: 3781 accountId = self.accountId # use `self.accountId` (main ID) by default 3782 3783 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3784 3785 self.body = str({"accountId": accountId}) 3786 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3787 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3788 3789 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3790 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3791 rawMargin = {} 3792 3793 else: 3794 if self.moreDebug: 3795 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3796 3797 return rawMargin 3798 3799 def RequestTariffLimits(self) -> dict: 3800 """ 3801 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3802 3803 See also: 3804 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3805 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3806 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3807 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3808 - `OverviewUserInfo()` method 3809 3810 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3811 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3812 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3813 """ 3814 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3815 3816 self.body = str({}) 3817 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3818 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3819 3820 if self.moreDebug: 3821 uLogger.debug("Records with limits of current tariff successfully received") 3822 3823 return rawTariffLimits 3824 3825 def RequestBondCoupons(self, iJSON: dict) -> dict: 3826 """ 3827 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3828 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3829 All dates are in UTC timezone. 3830 3831 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3832 Documentation: 3833 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3834 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3835 3836 See also: `ExtendBondsData()`. 3837 3838 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3839 If raw iJSON is not data of bond then server returns an error [400] with message: 3840 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3841 :return: dictionary with bond payment calendar. Response example 3842 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3843 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3844 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3845 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3846 """ 3847 if iJSON["figi"] is None or not iJSON["figi"]: 3848 uLogger.error("FIGI must be defined for using this method!") 3849 raise Exception("FIGI required") 3850 3851 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3852 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3853 3854 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3855 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3856 self.figi, 3857 startDate, 3858 endDate, 3859 )) 3860 3861 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3862 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3863 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3864 3865 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3866 uLogger.warning("Instrument type is not bond!") 3867 3868 else: 3869 if self.moreDebug: 3870 uLogger.debug("Records about bond payment calendar successfully received") 3871 3872 return calendar 3873 3874 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3875 """ 3876 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3877 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3878 coupon yields, current yields and some statistics etc. 3879 3880 WARNING! This is too long operation if a lot of bonds requested from broker server. 3881 3882 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3883 3884 :param instruments: list of strings with tickers or FIGIs. 3885 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3886 for further used by data scientists or stock analytics. 3887 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3888 In XLSX-file and Pandas DataFrame fields mean: 3889 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3890 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3891 """ 3892 if instruments is None or not instruments: 3893 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3894 raise Exception("Ticker or FIGI required") 3895 3896 if isinstance(instruments, str): 3897 instruments = [instruments] 3898 3899 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3900 3901 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3902 3903 iCount = len(uniqueInstruments) 3904 tooLong = iCount >= 20 3905 if tooLong: 3906 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3907 3908 bonds = None 3909 for i, self.figi in enumerate(uniqueInstruments): 3910 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3911 3912 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3913 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3914 rawBond = self.SearchByFIGI(requestPrice=True) 3915 3916 # Widen raw data with UTC current time (iData["actualDateTime"]): 3917 actualDate = datetime.now(tzutc()) 3918 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3919 3920 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3921 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3922 3923 # Replace some values with human-readable: 3924 iData["nominalCurrency"] = iData["nominal"]["currency"] 3925 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3926 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3927 iData["aciCurrency"] = iData["aciValue"]["currency"] 3928 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3929 iData["issueSize"] = int(iData["issueSize"]) 3930 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3931 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3932 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3933 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3934 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3935 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3936 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3937 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3938 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3939 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3940 3941 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3942 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3943 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3944 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3945 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3946 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3947 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3948 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3949 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3950 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3951 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3952 3953 # Widen raw data with calendar data from `rawCalendar` values: 3954 calendarData = [] 3955 if "events" in iData["rawCalendar"].keys(): 3956 for item in iData["rawCalendar"]["events"]: 3957 calendarData.append({ 3958 "couponDate": item["couponDate"], 3959 "couponNumber": int(item["couponNumber"]), 3960 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3961 "payCurrency": item["payOneBond"]["currency"], 3962 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3963 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3964 "couponStartDate": item["couponStartDate"], 3965 "couponEndDate": item["couponEndDate"], 3966 "couponPeriod": item["couponPeriod"], 3967 }) 3968 3969 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3970 if "maturityDate" not in iData.keys(): 3971 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3972 3973 # Widen raw data with Coupon Rate. 3974 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3975 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3976 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3977 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3978 3979 # Widen raw data with Yield to Maturity (YTM) on current date. 3980 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3981 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3982 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3983 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3984 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3985 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3986 3987 iData["calendar"] = calendarData # adds calendar at the end 3988 3989 # Remove not used data: 3990 iData.pop("uid") 3991 iData.pop("positionUid") 3992 iData.pop("currentPrice") 3993 iData.pop("rawCalendar") 3994 3995 colNames = list(iData.keys()) 3996 if bonds is None: 3997 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3998 3999 else: 4000 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4001 4002 else: 4003 uLogger.warning("Instrument is not a bond!") 4004 4005 processed = round(100 * (i + 1) / iCount, 1) 4006 if tooLong and processed % 5 == 0: 4007 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4008 4009 else: 4010 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4011 4012 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4013 4014 # Saving bonds from Pandas DataFrame to XLSX sheet: 4015 if xlsx and self.bondsXLSXFile: 4016 with pd.ExcelWriter( 4017 path=self.bondsXLSXFile, 4018 date_format=TKS_DATE_FORMAT, 4019 datetime_format=TKS_DATE_TIME_FORMAT, 4020 mode="w", 4021 ) as writer: 4022 bonds.to_excel( 4023 writer, 4024 sheet_name="Extended bonds data", 4025 index=True, 4026 encoding="UTF-8", 4027 freeze_panes=(1, 1), 4028 ) # saving as XLSX-file with freeze first row and column as headers 4029 4030 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4031 4032 return bonds 4033 4034 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4035 """ 4036 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4037 4038 WARNING! This is too long operation if a lot of bonds requested from broker server. 4039 4040 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4041 4042 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4043 extended information about bonds: main info, current prices, bond payment calendar, 4044 coupon yields, current yields and some statistics etc. 4045 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4046 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4047 for further used by data scientists or stock analytics. 4048 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4049 """ 4050 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4051 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4052 4053 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4054 4055 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4056 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4057 calendar = None 4058 for bond in extBonds.iterrows(): 4059 for item in bond[1]["calendar"]: 4060 cData = { 4061 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4062 "couponDate": item["couponDate"], 4063 "figi": bond[1]["figi"], 4064 "ticker": bond[1]["ticker"], 4065 "name": bond[1]["name"], 4066 "couponNumber": item["couponNumber"], 4067 "payOneBond": item["payOneBond"], 4068 "payCurrency": item["payCurrency"], 4069 "couponType": item["couponType"], 4070 "couponPeriod": item["couponPeriod"], 4071 "fixDate": item["fixDate"], 4072 "couponStartDate": item["couponStartDate"], 4073 "couponEndDate": item["couponEndDate"], 4074 } 4075 4076 if calendar is None: 4077 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4078 4079 else: 4080 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4081 4082 if calendar is not None: 4083 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4084 4085 # Saving calendar from Pandas DataFrame to XLSX sheet: 4086 if xlsx: 4087 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4088 4089 with pd.ExcelWriter( 4090 path=xlsxCalendarFile, 4091 date_format=TKS_DATE_FORMAT, 4092 datetime_format=TKS_DATE_TIME_FORMAT, 4093 mode="w", 4094 ) as writer: 4095 humanReadable = calendar.copy(deep=True) 4096 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4097 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4098 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4099 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4100 humanReadable.columns = colNames # human-readable column names 4101 4102 humanReadable.to_excel( 4103 writer, 4104 sheet_name="Bond payments calendar", 4105 index=False, 4106 encoding="UTF-8", 4107 freeze_panes=(1, 2), 4108 ) # saving as XLSX-file with freeze first row and column as headers 4109 4110 del humanReadable # release df in memory 4111 4112 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4113 4114 return calendar 4115 4116 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4117 """ 4118 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4119 Also, creates Markdown file with calendar data, `calendar.md` by default. 4120 4121 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4122 4123 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4124 extended information about bonds: main info, current prices, bond payment calendar, 4125 coupon yields, current yields and some statistics etc. 4126 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4127 :param show: if `True` then also printing bonds payment calendar to the console, 4128 otherwise save to file `calendarFile` only. `False` by default. 4129 :return: multilines text in Markdown format with bonds payment calendar as a table. 4130 """ 4131 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4132 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4133 4134 infoText = "# Bond payments calendar\n\n" 4135 4136 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4137 4138 if not (calendar is None or calendar.empty): 4139 splitLine = "| | | | | | | | | |\n" 4140 4141 info = [ 4142 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4143 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4144 ] 4145 4146 newMonth = False 4147 notOneBond = calendar["figi"].nunique() > 1 4148 for i, bond in enumerate(calendar.iterrows()): 4149 if newMonth and notOneBond: 4150 info.append(splitLine) 4151 4152 info.append( 4153 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4154 " √" if bond[1]["paid"] else " —", 4155 bond[1]["couponDate"].split("T")[0], 4156 bond[1]["figi"], 4157 bond[1]["ticker"], 4158 bond[1]["couponNumber"], 4159 "{} {}".format( 4160 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4161 bond[1]["payCurrency"], 4162 ), 4163 bond[1]["couponType"], 4164 bond[1]["couponPeriod"], 4165 bond[1]["fixDate"].split("T")[0], 4166 ) 4167 ) 4168 4169 if i < len(calendar.values) - 1: 4170 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4171 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4172 newMonth = False if curDate.month == nextDate.month else True 4173 4174 else: 4175 newMonth = False 4176 4177 infoText += "".join(info) 4178 4179 if show: 4180 uLogger.info("{}".format(infoText)) 4181 4182 if self.calendarFile is not None: 4183 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4184 fH.write(infoText) 4185 4186 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4187 4188 else: 4189 infoText += "No data\n" 4190 4191 return infoText 4192 4193 def OverviewAccounts(self, show: bool = False) -> dict: 4194 """ 4195 Method for parsing and show simple table with all available user accounts. 4196 4197 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4198 4199 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4200 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4201 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4202 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4203 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4204 "closed": "—", "access": "Full access" }, ...}}` 4205 """ 4206 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4207 4208 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4209 accounts = { 4210 item["id"]: { 4211 "type": TKS_ACCOUNT_TYPES[item["type"]], 4212 "name": item["name"], 4213 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4214 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4215 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4216 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4217 } for item in rawAccounts["accounts"] 4218 } 4219 4220 # Raw and parsed data with some fields replaced in "stat" section: 4221 view = { 4222 "rawAccounts": rawAccounts, 4223 "stat": accounts, 4224 } 4225 4226 # --- Prepare simple text table with only accounts data in human-readable format: 4227 if show: 4228 info = [ 4229 "# User accounts\n\n", 4230 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4231 "| Account ID | Type | Status | Name |\n", 4232 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4233 ] 4234 4235 for account in view["stat"].keys(): 4236 info.extend([ 4237 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4238 account, 4239 view["stat"][account]["type"], 4240 view["stat"][account]["status"], 4241 view["stat"][account]["name"], 4242 ) 4243 ]) 4244 4245 infoText = "".join(info) 4246 4247 uLogger.info(infoText) 4248 4249 if self.userAccountsFile: 4250 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4251 fH.write(infoText) 4252 4253 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4254 4255 return view 4256 4257 def OverviewUserInfo(self, show: bool = False) -> dict: 4258 """ 4259 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4260 4261 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4262 4263 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4264 :return: dict with raw parsed data from server and some calculated statistics about it. 4265 """ 4266 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4267 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4268 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4269 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4270 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4271 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4272 4273 # This is dict with parsed common user data: 4274 userInfo = { 4275 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4276 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4277 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4278 "tariff": rawUserInfo["tariff"], 4279 } 4280 4281 # This is an array of dict with parsed margin statuses for every account IDs: 4282 margins = {} 4283 for accountId in accounts.keys(): 4284 if rawMargins[accountId]: 4285 margins[accountId] = { 4286 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4287 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4288 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4289 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4290 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4291 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4292 } 4293 4294 else: 4295 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4296 4297 unary = {} # unary-connection limits 4298 for item in rawTariffLimits["unaryLimits"]: 4299 if item["limitPerMinute"] in unary.keys(): 4300 unary[item["limitPerMinute"]].extend(item["methods"]) 4301 4302 else: 4303 unary[item["limitPerMinute"]] = item["methods"] 4304 4305 stream = {} # stream-connection limits 4306 for item in rawTariffLimits["streamLimits"]: 4307 if item["limit"] in stream.keys(): 4308 stream[item["limit"]].extend(item["streams"]) 4309 4310 else: 4311 stream[item["limit"]] = item["streams"] 4312 4313 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4314 limits = { 4315 "unary": unary, 4316 "stream": stream, 4317 } 4318 4319 # Raw and parsed data as an output result: 4320 view = { 4321 "rawUserInfo": rawUserInfo, 4322 "rawAccounts": rawAccounts, 4323 "rawMargins": rawMargins, 4324 "rawTariffLimits": rawTariffLimits, 4325 "stat": { 4326 "userInfo": userInfo, 4327 "accounts": accounts, 4328 "margins": margins, 4329 "limits": limits, 4330 }, 4331 } 4332 4333 # --- Prepare text table with user information in human-readable format: 4334 if show: 4335 info = [ 4336 "# Full user information\n\n", 4337 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4338 "## Common information\n\n", 4339 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4340 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4341 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4342 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4343 "\n## User accounts\n\n", 4344 ] 4345 4346 for account in view["stat"]["accounts"].keys(): 4347 info.extend([ 4348 "### ID: [{}]\n\n".format(account), 4349 "| Parameters | Values |\n", 4350 "|----------------------|--------------------------------------------------------------|\n", 4351 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4352 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4353 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4354 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4355 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4356 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4357 ]) 4358 4359 if margins[account]: 4360 info.extend([ 4361 "| Margin status: | Enabled |\n", 4362 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4363 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4364 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4365 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4366 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4367 ]) 4368 4369 else: 4370 info.append("| Margin status: | Disabled |\n\n") 4371 4372 info.extend([ 4373 "\n## Current user tariff limits\n", 4374 "\nSee also:\n", 4375 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4376 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4377 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4378 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4379 "\n### Unary limits\n", 4380 ]) 4381 4382 if unary: 4383 for key, values in sorted(unary.items()): 4384 info.append("\n* Max requests per minute: {}\n".format(key)) 4385 4386 for value in values: 4387 info.append(" - {}\n".format(value)) 4388 4389 else: 4390 info.append("\nNot available\n") 4391 4392 info.append("\n### Stream limits\n") 4393 4394 if stream: 4395 for key, values in sorted(stream.items()): 4396 info.append("\n* Max stream connections: {}\n".format(key)) 4397 4398 for value in values: 4399 info.append(" - {}\n".format(value)) 4400 4401 else: 4402 info.append("\nNot available\n") 4403 4404 infoText = "".join(info) 4405 4406 uLogger.info(infoText) 4407 4408 if self.userInfoFile: 4409 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4410 fH.write(infoText) 4411 4412 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4413 4414 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.moreDebug = False 301 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 302 303 self.historyFile = None 304 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 305 306 See also: `History()`. 307 """ 308 309 self.htmlHistoryFile = "index.html" 310 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 311 312 See also: `ShowHistoryChart()`. 313 """ 314 315 self.instrumentsFile = "instruments.md" 316 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 317 318 See also: `ShowInstrumentsInfo()`. 319 """ 320 321 self.searchResultsFile = "search-results.md" 322 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 323 324 See also: `SearchInstruments()`. 325 """ 326 327 self.pricesFile = "prices.md" 328 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 329 330 See also: `GetListOfPrices()`. 331 """ 332 333 self.infoFile = "info.md" 334 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 335 336 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 337 """ 338 339 self.bondsXLSXFile = "ext-bonds.xlsx" 340 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 341 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 342 343 See also: `ExtendBondsData()`. 344 """ 345 346 self.calendarFile = "calendar.md" 347 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 348 349 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 350 351 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 352 """ 353 354 self.overviewFile = "overview.md" 355 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 356 357 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 358 """ 359 360 self.overviewDigestFile = "overview-digest.md" 361 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 362 363 See also: `Overview()` with parameter `details="digest"`. 364 """ 365 366 self.overviewPositionsFile = "overview-positions.md" 367 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 368 369 See also: `Overview()` with parameter `details="positions"`. 370 """ 371 372 self.overviewOrdersFile = "overview-orders.md" 373 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 374 375 See also: `Overview()` with parameter `details="orders"`. 376 """ 377 378 self.overviewAnalyticsFile = "overview-analytics.md" 379 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 380 381 See also: `Overview()` with parameter `details="analytics"`. 382 """ 383 384 self.reportFile = "deals.md" 385 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 386 387 See also: `Deals()`. 388 """ 389 390 self.withdrawalLimitsFile = "limits.md" 391 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 392 393 See also: `OverviewLimits()` and `RequestLimits()`. 394 """ 395 396 self.userInfoFile = "user-info.md" 397 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 398 399 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 400 """ 401 402 self.userAccountsFile = "accounts.md" 403 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 404 405 See also: `OverviewAccounts()`, `RequestAccounts()`. 406 """ 407 408 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 409 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 410 411 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 412 413 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 414 """ 415 416 self.iList = None # init iList for raw instruments data 417 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 418 419 See also: `Listing()`, `DumpInstruments()`. 420 """ 421 422 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 423 if useCache: 424 if os.path.exists(self.iListDumpFile): 425 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 426 curTime = datetime.now(tzutc()) 427 428 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 429 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 430 431 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 432 433 else: 434 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 435 436 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 437 os.path.abspath(self.iListDumpFile), 438 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 439 )) 440 441 else: 442 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 443 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 444 445 else: 446 self.iList = self.Listing() # request new raw instruments data from broker server 447 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 448 449 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 450 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 451 452 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 453 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
469 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 470 """ 471 Send GET or POST request to broker server and receive JSON object. 472 473 self.header: must be defining with dictionary of headers. 474 self.body: if define then used as request body. None by default. 475 self.timeout: global request timeout, 15 seconds by default. 476 :param url: url with REST request. 477 :param reqType: send "GET" or "POST" request. "GET" by default. 478 :param retry: how many times retry after first request if an 5xx server errors occurred. 479 :param pause: sleep time in seconds between retries. 480 :return: response JSON (dictionary) from broker. 481 """ 482 if reqType not in ("GET", "POST"): 483 uLogger.error("You can define request type: 'GET' or 'POST'!") 484 raise Exception("Incorrect value") 485 486 if self.moreDebug: 487 uLogger.debug("Request parameters:") 488 uLogger.debug(" - REST API URL: {}".format(url)) 489 uLogger.debug(" - request type: {}".format(reqType)) 490 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 491 uLogger.debug(" - body:\n{}".format(self.body)) 492 493 # fast hack to avoid all operations with some tickers/FIGI 494 responseJSON = {} 495 oK = True 496 for item in self.exclude: 497 if item in url: 498 if self.moreDebug: 499 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 500 501 oK = False 502 break 503 504 if oK: 505 counter = 0 506 response = None 507 errMsg = "" 508 509 while not response and counter <= retry: 510 if reqType == "GET": 511 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 512 513 if reqType == "POST": 514 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 515 516 if self.moreDebug: 517 uLogger.debug("Response:") 518 uLogger.debug(" - status code: {}".format(response.status_code)) 519 uLogger.debug(" - reason: {}".format(response.reason)) 520 uLogger.debug(" - body length: {}".format(len(response.text))) 521 uLogger.debug(" - headers:\n{}".format(response.headers)) 522 523 # Server returns some headers: 524 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 525 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 526 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 527 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 528 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 529 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 530 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 531 sleep(rateLimitWait) 532 533 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 534 if 400 <= response.status_code < 500: 535 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 536 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 537 counter = retry + 1 538 539 if 500 <= response.status_code < 600: 540 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 541 uLogger.debug(" - not oK, {}".format(errMsg)) 542 counter += 1 543 544 if counter <= retry: 545 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 546 sleep(pause) 547 548 responseJSON = self._ParseJSON(rawData=response.text) 549 550 if errMsg: 551 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 552 uLogger.error(" - not oK, {}".format(errMsg)) 553 554 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
587 def Listing(self) -> dict: 588 """ 589 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 590 591 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 592 """ 593 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 594 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 595 596 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 597 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 598 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 599 600 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 601 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 602 poolUpdater.close() 603 604 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 605 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 606 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 607 608 # calculate minimum price increment (step) for all instruments and set up instrument's type: 609 for iType in iList.keys(): 610 for ticker in iList[iType]: 611 iList[iType][ticker]["type"] = iType 612 613 if "minPriceIncrement" in iList[iType][ticker].keys(): 614 iList[iType][ticker]["step"] = NanoToFloat( 615 iList[iType][ticker]["minPriceIncrement"]["units"], 616 iList[iType][ticker]["minPriceIncrement"]["nano"], 617 ) 618 619 else: 620 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 621 622 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
624 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 625 """ 626 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 627 628 See also: `DumpInstruments()`, `Listing()`. 629 630 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 631 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 632 """ 633 if self.iListDumpFile is None or not self.iListDumpFile: 634 uLogger.error("Output name of dump file must be defined!") 635 raise Exception("Filename required") 636 637 if not self.iList or forceUpdate: 638 self.iList = self.Listing() 639 640 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 641 642 # Save as XLSX with separated sheets for every type of instruments: 643 with pd.ExcelWriter( 644 path=xlsxDumpFile, 645 date_format=TKS_DATE_FORMAT, 646 datetime_format=TKS_DATE_TIME_FORMAT, 647 mode="w", 648 ) as writer: 649 for iType in TKS_INSTRUMENTS: 650 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 651 df = df[sorted(df)] # sorted by column names 652 df = df.applymap( 653 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 654 na_action="ignore", 655 ) # converting numbers from nano-type to float in every cell 656 df.to_excel( 657 writer, 658 sheet_name=iType, 659 encoding="UTF-8", 660 freeze_panes=(1, 1), 661 ) # saving as XLSX-file with freeze first row and column as headers 662 663 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
665 def DumpInstruments(self, forceUpdate: bool = True) -> str: 666 """ 667 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 668 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 669 670 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 671 672 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 673 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 674 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 675 """ 676 if self.iListDumpFile is None or not self.iListDumpFile: 677 uLogger.error("Output name of dump file must be defined!") 678 raise Exception("Filename required") 679 680 if not self.iList or forceUpdate: 681 self.iList = self.Listing() 682 683 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 684 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 685 fH.write(jsonDump) 686 687 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 688 689 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
691 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 692 """ 693 Show information about one instrument defined by json data and prints it in Markdown format. 694 695 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 696 697 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 698 :param show: if `True` then also printing information about instrument and its current price. 699 :return: multilines text in Markdown format with information about one instrument. 700 """ 701 splitLine = "| | |\n" 702 infoText = "" 703 704 if iJSON is not None and iJSON and isinstance(iJSON, dict): 705 info = [ 706 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 707 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 708 "| Parameters | Values |\n", 709 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 710 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 711 "| Full name: | {:<54} |\n".format(iJSON["name"]), 712 ] 713 714 if "sector" in iJSON.keys() and iJSON["sector"]: 715 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 716 717 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 718 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 719 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 720 ))) 721 722 info.extend([ 723 splitLine, 724 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 725 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 726 ]) 727 728 if "isin" in iJSON.keys() and iJSON["isin"]: 729 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 730 731 if "classCode" in iJSON.keys(): 732 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 733 734 info.extend([ 735 splitLine, 736 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 737 splitLine, 738 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 739 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 740 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 741 ]) 742 743 if iJSON["figi"]: 744 self.figi = iJSON["figi"] 745 iJSON = iJSON | self.RequestTradingStatus() 746 747 info.extend([ 748 splitLine, 749 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 750 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 751 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 752 ]) 753 754 info.append(splitLine) 755 756 if "type" in iJSON.keys() and iJSON["type"]: 757 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 758 759 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 760 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 761 762 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 763 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 764 765 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 766 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 767 768 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 769 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 770 771 if "focusType" in iJSON.keys() and iJSON["focusType"]: 772 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 773 774 if "assetType" in iJSON.keys() and iJSON["assetType"]: 775 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 776 777 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 778 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 779 780 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 781 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 782 783 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 784 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 785 786 if "currency" in iJSON.keys(): 787 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 788 789 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 790 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 791 792 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 793 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 794 795 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 796 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 797 798 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 799 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 800 801 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 802 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 803 804 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 805 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 806 807 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 808 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 809 810 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 811 info.append("| Perpetual bond: | Yes |\n") 812 813 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 814 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 815 816 iExt = None 817 if iJSON["type"] == "Bonds": 818 info.extend([ 819 splitLine, 820 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 821 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 822 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 823 iJSON["nominal"]["currency"], 824 )), 825 ]) 826 827 if "floatingCouponFlag" in iJSON.keys(): 828 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 829 830 if "amortizationFlag" in iJSON.keys(): 831 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 832 833 info.append(splitLine) 834 835 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 836 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 837 838 if iJSON["figi"]: 839 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 840 841 info.extend([ 842 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 843 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 844 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 845 ]) 846 847 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 848 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 849 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 850 iJSON["aciValue"]["currency"] 851 ))) 852 853 if "currentPrice" in iJSON.keys(): 854 info.append(splitLine) 855 856 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 857 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 858 859 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 860 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 861 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 862 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 863 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 864 865 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 866 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 867 868 info.extend([ 869 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 870 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 871 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 872 )), 873 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 874 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 875 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 876 )), 877 "| Changes between last deal price and last close | {:<54} |\n".format( 878 "{:.2f}%{}".format( 879 iJSON["currentPrice"]["changes"], 880 " ({}{:.2f} {})".format( 881 "+" if bondChangesDelta > 0 else "", 882 bondChangesDelta, 883 aciCurrency 884 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 885 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 886 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 887 currency 888 ), 889 ) 890 ), 891 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 892 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 893 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 894 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 897 )), 898 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 899 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 903 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 904 )), 905 ]) 906 907 if "lot" in iJSON.keys(): 908 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 909 910 if "step" in iJSON.keys() and iJSON["step"] != 0: 911 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 912 913 # Add bond payment calendar: 914 if iJSON["type"] == "Bonds": 915 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 916 info.extend(["\n", strCalendar]) 917 918 infoText += "".join(info) 919 920 if show: 921 uLogger.info("{}".format(infoText)) 922 923 else: 924 uLogger.debug("{}".format(infoText)) 925 926 if self.infoFile is not None: 927 with open(self.infoFile, "w", encoding="UTF-8") as fH: 928 fH.write(infoText) 929 930 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 931 932 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
934 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 935 """ 936 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 937 938 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 939 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 940 :return: JSON formatted data with information about instrument. 941 """ 942 tickerJSON = {} 943 if self.moreDebug: 944 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 945 946 if not self.ticker: 947 uLogger.warning("self.ticker variable is not be empty!") 948 949 else: 950 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 951 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 952 raise Exception("Instrument not allowed") 953 954 if not self.iList: 955 self.iList = self.Listing() 956 957 if self.ticker in self.iList["Shares"].keys(): 958 tickerJSON = self.iList["Shares"][self.ticker] 959 if self.moreDebug: 960 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 961 962 elif self.ticker in self.iList["Currencies"].keys(): 963 tickerJSON = self.iList["Currencies"][self.ticker] 964 if self.moreDebug: 965 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Bonds"].keys(): 968 tickerJSON = self.iList["Bonds"][self.ticker] 969 if self.moreDebug: 970 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Etfs"].keys(): 973 tickerJSON = self.iList["Etfs"][self.ticker] 974 if self.moreDebug: 975 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Futures"].keys(): 978 tickerJSON = self.iList["Futures"][self.ticker] 979 if self.moreDebug: 980 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 981 982 if tickerJSON: 983 self.figi = tickerJSON["figi"] 984 985 if requestPrice: 986 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 987 988 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 989 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 990 991 else: 992 tickerJSON["currentPrice"]["changes"] = 0 993 994 if show: 995 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 996 997 else: 998 if show: 999 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1000 1001 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1003 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1004 """ 1005 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1006 1007 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1008 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1009 :return: JSON formatted data with information about instrument. 1010 """ 1011 figiJSON = {} 1012 if self.moreDebug: 1013 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1014 1015 if not self.figi: 1016 uLogger.warning("self.figi variable is not be empty!") 1017 1018 else: 1019 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1020 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1021 raise Exception("Instrument not allowed") 1022 1023 if not self.iList: 1024 self.iList = self.Listing() 1025 1026 for item in self.iList["Shares"].keys(): 1027 if self.figi == self.iList["Shares"][item]["figi"]: 1028 figiJSON = self.iList["Shares"][item] 1029 1030 if self.moreDebug: 1031 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1032 1033 break 1034 1035 if not figiJSON: 1036 for item in self.iList["Currencies"].keys(): 1037 if self.figi == self.iList["Currencies"][item]["figi"]: 1038 figiJSON = self.iList["Currencies"][item] 1039 1040 if self.moreDebug: 1041 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1042 1043 break 1044 1045 if not figiJSON: 1046 for item in self.iList["Bonds"].keys(): 1047 if self.figi == self.iList["Bonds"][item]["figi"]: 1048 figiJSON = self.iList["Bonds"][item] 1049 1050 if self.moreDebug: 1051 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1052 1053 break 1054 1055 if not figiJSON: 1056 for item in self.iList["Etfs"].keys(): 1057 if self.figi == self.iList["Etfs"][item]["figi"]: 1058 figiJSON = self.iList["Etfs"][item] 1059 1060 if self.moreDebug: 1061 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1062 1063 break 1064 1065 if not figiJSON: 1066 for item in self.iList["Futures"].keys(): 1067 if self.figi == self.iList["Futures"][item]["figi"]: 1068 figiJSON = self.iList["Futures"][item] 1069 1070 if self.moreDebug: 1071 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1072 1073 break 1074 1075 if figiJSON: 1076 self.figi = figiJSON["figi"] 1077 self.ticker = figiJSON["ticker"] 1078 1079 if requestPrice: 1080 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1081 1082 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1083 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1084 1085 else: 1086 figiJSON["currentPrice"]["changes"] = 0 1087 1088 if show: 1089 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1090 1091 else: 1092 if show: 1093 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1094 1095 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1097 def GetCurrentPrices(self, show: bool = True) -> dict: 1098 """ 1099 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1100 `{"buy": [{"price": 1243.8, "quantity": 193}, 1101 {"price": 1244.0, "quantity": 168}, 1102 {"price": 1244.8, "quantity": 5}, 1103 {"price": 1245.0, "quantity": 61}, 1104 {"price": 1245.4, "quantity": 60}], 1105 "sell": [{"price": 1243.6, "quantity": 8}, 1106 {"price": 1242.6, "quantity": 10}, 1107 {"price": 1242.4, "quantity": 18}, 1108 {"price": 1242.2, "quantity": 50}, 1109 {"price": 1242.0, "quantity": 113}], 1110 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1111 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1112 - sell: list of dicts with Buyers prices, 1113 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1114 - quantity: volume value by current price in lots, 1115 - limitUp: current trade session limit price, maximum, 1116 - limitDown: current trade session limit price, minimum, 1117 - lastPrice: last deal price of the instrument, 1118 - closePrice: previous trade session close price of the instrument. 1119 1120 See also: `SearchByTicker()` and `SearchByFIGI()`. 1121 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1122 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1123 1124 :param show: if `True` then print DOM to log and console. 1125 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1126 If an error occurred then returns an empty record: 1127 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1128 """ 1129 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1130 1131 if self.depth < 1: 1132 uLogger.error("Depth of Market (DOM) must be >=1!") 1133 raise Exception("Incorrect value") 1134 1135 if not (self.ticker or self.figi): 1136 uLogger.error("self.ticker or self.figi variables must be defined!") 1137 raise Exception("Ticker or FIGI required") 1138 1139 if self.ticker and not self.figi: 1140 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1141 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1142 1143 if not self.ticker and self.figi: 1144 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1145 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1146 1147 if not self.figi: 1148 uLogger.error("FIGI is not defined!") 1149 raise Exception("Ticker or FIGI required") 1150 1151 else: 1152 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1153 1154 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1155 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1156 self.body = str({"figi": self.figi, "depth": self.depth}) 1157 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1158 1159 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1160 # list of dicts with sellers orders: 1161 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1162 1163 # list of dicts with buyers orders: 1164 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1165 1166 # max price of instrument at this time: 1167 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1168 1169 # min price of instrument at this time: 1170 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1171 1172 # last price of deal with instrument: 1173 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1174 1175 # last close price of instrument: 1176 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1177 1178 else: 1179 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1180 uLogger.debug("Server response: {}".format(pricesResponse)) 1181 1182 if show: 1183 if prices["buy"] or prices["sell"]: 1184 info = [ 1185 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1186 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1187 self.ticker, 1188 self.figi, 1189 self.depth, 1190 ), 1191 "-" * 60, "\n", 1192 " Orders of Buyers | Orders of Sellers\n", 1193 "-" * 60, "\n", 1194 " Sell prices (volumes) | Buy prices (volumes)\n", 1195 "-" * 60, "\n", 1196 ] 1197 1198 if not prices["buy"]: 1199 info.append(" | No orders!\n") 1200 sumBuy = 0 1201 1202 else: 1203 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1204 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1205 for item in maxMinSorted: 1206 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1207 1208 if not prices["sell"]: 1209 info.append("No orders! |\n") 1210 sumSell = 0 1211 1212 else: 1213 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1214 for item in prices["sell"]: 1215 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1216 1217 info.extend([ 1218 "-" * 60, "\n", 1219 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1220 "-" * 60, "\n", 1221 ]) 1222 1223 infoText = "".join(info) 1224 1225 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1226 1227 else: 1228 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1229 1230 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1232 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1233 """ 1234 This method get and show information about all available broker instruments for current user account. 1235 If `instrumentsFile` string is not empty then also save information to this file. 1236 1237 :param show: if `True` then print results to console, if `False` - print only to file. 1238 :return: multi-lines string with all available broker instruments 1239 """ 1240 if not self.iList: 1241 self.iList = self.Listing() 1242 1243 info = [ 1244 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1245 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1246 ] 1247 1248 # add instruments count by type: 1249 for iType in self.iList.keys(): 1250 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1251 1252 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1253 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1254 1255 # generating info tables with all instruments by type: 1256 for iType in self.iList.keys(): 1257 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1258 1259 for instrument in self.iList[iType].keys(): 1260 iName = self.iList[iType][instrument]["name"] # instrument's name 1261 if len(iName) > 57: 1262 iName = "{}...".format(iName[:54]) # right trim for a long string 1263 1264 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1265 self.iList[iType][instrument]["ticker"], 1266 iName, 1267 self.iList[iType][instrument]["figi"], 1268 self.iList[iType][instrument]["currency"], 1269 self.iList[iType][instrument]["lot"], 1270 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1271 )) 1272 1273 infoText = "".join(info) 1274 1275 if show: 1276 uLogger.info(infoText) 1277 1278 if self.instrumentsFile: 1279 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1280 fH.write(infoText) 1281 1282 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1283 1284 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1286 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1287 """ 1288 This method search and show information about instruments by part of its ticker, FIGI or name. 1289 If `searchResultsFile` string is not empty then also save information to this file. 1290 1291 :param pattern: string with part of ticker, FIGI or instrument's name. 1292 :param show: if `True` then print results to console, if `False` - return list of result only. 1293 :return: list of dictionaries with all found instruments. 1294 """ 1295 if not self.iList: 1296 self.iList = self.Listing() 1297 1298 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1299 compiledPattern = re.compile(pattern, re.IGNORECASE) 1300 1301 for iType in self.iList: 1302 for instrument in self.iList[iType].values(): 1303 searchResult = compiledPattern.search(" ".join( 1304 [instrument["ticker"], instrument["figi"], instrument["name"]] 1305 )) 1306 1307 if searchResult: 1308 searchResults[iType][instrument["ticker"]] = instrument 1309 1310 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1311 info = [ 1312 "# Search results\n\n", 1313 "* **Search pattern:** [{}]\n".format(pattern), 1314 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1315 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1316 ] 1317 infoShort = info[:] 1318 1319 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1320 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1321 skippedLine = "| ... | ... | ... | ... |\n" 1322 1323 if resultsLen == 0: 1324 info.append("\nNo results\n") 1325 infoShort.append("\nNo results\n") 1326 uLogger.warning("No results. Try changing your search pattern.") 1327 1328 else: 1329 for iType in searchResults: 1330 iTypeValuesCount = len(searchResults[iType].values()) 1331 if iTypeValuesCount > 0: 1332 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1333 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1334 1335 for instrument in searchResults[iType].values(): 1336 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1337 instrument["type"], 1338 instrument["ticker"], 1339 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1340 instrument["figi"], 1341 )) 1342 1343 if iTypeValuesCount <= 5: 1344 infoShort.extend(info[-iTypeValuesCount:]) 1345 1346 else: 1347 infoShort.extend(info[-5:]) 1348 infoShort.append(skippedLine) 1349 1350 infoText = "".join(info) 1351 infoTextShort = "".join(infoShort) 1352 1353 if show: 1354 uLogger.info(infoTextShort) 1355 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1356 1357 if self.searchResultsFile: 1358 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1359 fH.write(infoText) 1360 1361 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1362 1363 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1365 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1366 """ 1367 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1368 1369 :param instruments: list of strings with tickers or FIGIs. 1370 :return: list with unique instrument FIGIs only. 1371 """ 1372 requestedInstruments = [] 1373 for iName in instruments: 1374 if iName not in self.aliases.keys(): 1375 if iName not in requestedInstruments: 1376 requestedInstruments.append(iName) 1377 1378 else: 1379 if iName not in requestedInstruments: 1380 if self.aliases[iName] not in requestedInstruments: 1381 requestedInstruments.append(self.aliases[iName]) 1382 1383 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1384 1385 onlyUniqueFIGIs = [] 1386 for iName in requestedInstruments: 1387 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1388 continue 1389 1390 self.ticker = iName 1391 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1392 1393 if not iData: 1394 self.ticker = "" 1395 self.figi = iName 1396 1397 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1398 1399 if not iData: 1400 self.figi = "" 1401 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1402 1403 if iData and iData["figi"] not in onlyUniqueFIGIs: 1404 onlyUniqueFIGIs.append(iData["figi"]) 1405 1406 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1407 1408 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1410 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1411 """ 1412 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1413 See limits: https://tinkoff.github.io/investAPI/limits/ 1414 If `pricesFile` string is not empty then also save information to this file. 1415 1416 :param instruments: list of strings with tickers or FIGIs. 1417 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1418 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1419 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1420 """ 1421 if instruments is None or not instruments: 1422 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1423 raise Exception("Ticker or FIGI required") 1424 1425 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1426 1427 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1428 1429 iList = [] # trying to get info and current prices about all unique instruments: 1430 for self.figi in onlyUniqueFIGIs: 1431 iData = self.SearchByFIGI(requestPrice=True) 1432 iList.append(iData) 1433 1434 self.ShowListOfPrices(iList, show) 1435 1436 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1438 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1439 """ 1440 Show table contains current prices of given instruments. 1441 1442 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1443 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1444 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1445 :return: multilines text in Markdown format as a table contains current prices. 1446 """ 1447 infoText = "" 1448 1449 if show or self.pricesFile: 1450 info = [ 1451 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1452 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1453 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1454 ] 1455 1456 for item in iList: 1457 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1458 item["ticker"], 1459 item["figi"], 1460 item["type"], 1461 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1462 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1463 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1464 "{} / {}".format( 1465 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1466 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1467 ), 1468 "{} / {}".format( 1469 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1470 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1471 ), 1472 item["currency"], 1473 )) 1474 1475 infoText = "".join(info) 1476 1477 if show: 1478 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1479 1480 if self.pricesFile: 1481 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1482 fH.write(infoText) 1483 1484 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1485 1486 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1488 def RequestTradingStatus(self) -> dict: 1489 """ 1490 Requesting trading status for the instrument defined by `figi` variable. 1491 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1492 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1493 1494 :return: dictionary with trading status attributes. Response example: 1495 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1496 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1497 """ 1498 if self.figi is None or not self.figi: 1499 uLogger.error("Variable `figi` must be defined for using this method!") 1500 raise Exception("FIGI required") 1501 1502 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1503 1504 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1505 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1506 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1507 1508 if self.moreDebug: 1509 uLogger.debug("Records about current trading status successfully received") 1510 1511 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1513 def RequestPortfolio(self) -> dict: 1514 """ 1515 Requesting actual user's portfolio for current `accountId`. 1516 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1517 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1518 1519 :return: dictionary with user's portfolio. 1520 """ 1521 if self.accountId is None or not self.accountId: 1522 uLogger.error("Variable `accountId` must be defined for using this method!") 1523 raise Exception("Account ID required") 1524 1525 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1526 1527 self.body = str({"accountId": self.accountId}) 1528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1529 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1530 1531 if self.moreDebug: 1532 uLogger.debug("Records about user's portfolio successfully received") 1533 1534 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1536 def RequestPositions(self) -> dict: 1537 """ 1538 Requesting open positions by currencies and instruments for current `accountId`. 1539 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1540 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1541 1542 :return: dictionary with open positions by instruments. 1543 """ 1544 if self.accountId is None or not self.accountId: 1545 uLogger.error("Variable `accountId` must be defined for using this method!") 1546 raise Exception("Account ID required") 1547 1548 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1549 1550 self.body = str({"accountId": self.accountId}) 1551 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1552 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1553 1554 if self.moreDebug: 1555 uLogger.debug("Records about current open positions successfully received") 1556 1557 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1559 def RequestPendingOrders(self) -> list: 1560 """ 1561 Requesting current actual pending orders for current `accountId`. 1562 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1563 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1564 1565 :return: list of dictionaries with pending orders. 1566 """ 1567 if self.accountId is None or not self.accountId: 1568 uLogger.error("Variable `accountId` must be defined for using this method!") 1569 raise Exception("Account ID required") 1570 1571 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1572 1573 self.body = str({"accountId": self.accountId}) 1574 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1575 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1576 1577 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1578 1579 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1581 def RequestStopOrders(self) -> list: 1582 """ 1583 Requesting current actual stop orders for current `accountId`. 1584 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1585 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1586 1587 :return: list of dictionaries with stop orders. 1588 """ 1589 if self.accountId is None or not self.accountId: 1590 uLogger.error("Variable `accountId` must be defined for using this method!") 1591 raise Exception("Account ID required") 1592 1593 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1594 1595 self.body = str({"accountId": self.accountId}) 1596 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1597 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1598 1599 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1600 1601 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1603 def Overview(self, show: bool = False, details: str = "full") -> dict: 1604 """ 1605 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1606 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1607 are defined then also save information to file. 1608 1609 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1610 many requests about the state of the portfolio, and then, based on the received data, a large number 1611 of calculation and statistics are collected. 1612 1613 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1614 :param details: how detailed should the information be? You should specify one of strings: 1615 `full` - shows full available information about portfolio status (by default), 1616 `positions` - shows only open positions, 1617 `digest` - show a short digest of the portfolio status, 1618 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1619 `orders` - shows only sections of open limits and stop orders. 1620 :return: dictionary with client's raw portfolio and some statistics. 1621 """ 1622 if self.accountId is None or not self.accountId: 1623 uLogger.error("Variable `accountId` must be defined for using this method!") 1624 raise Exception("Account ID required") 1625 1626 view = { 1627 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1628 "headers": {}, # list of dictionaries, response headers without "positions" section 1629 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1630 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1631 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1632 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1633 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1634 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1635 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1636 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1637 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1638 }, 1639 "stat": { # --- some statistics calculated using "raw" sections: 1640 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1641 "availableRUB": 0., # available rubles (without other currencies) 1642 "blockedRUB": 0., # blocked sum in Russian Rouble 1643 "totalChangesRUB": 0., # changes for all open trades in RUB 1644 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1645 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1646 "sharesCostRUB": 0., # costs of all shares in RUB 1647 "bondsCostRUB": 0., # costs of all bonds in RUB 1648 "etfsCostRUB": 0., # costs of all etfs in RUB 1649 "futuresCostRUB": 0., # costs of all futures in RUB 1650 "Currencies": [], # list of dictionaries of all currencies statistics 1651 "Shares": [], # list of dictionaries of all shares statistics 1652 "Bonds": [], # list of dictionaries of all bonds statistics 1653 "Etfs": [], # list of dictionaries of all etfs statistics 1654 "Futures": [], # list of dictionaries of all futures statistics 1655 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1656 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1657 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1658 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1659 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1660 }, 1661 "analytics": { # --- some analytics of portfolio: 1662 "distrByAssets": {}, # portfolio distribution by assets 1663 "distrByCompanies": {}, # portfolio distribution by companies 1664 "distrBySectors": {}, # portfolio distribution by sectors 1665 "distrByCurrencies": {}, # portfolio distribution by currencies 1666 "distrByCountries": {}, # portfolio distribution by countries 1667 } 1668 } 1669 1670 details = details.lower() 1671 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1672 if details not in availableDetails: 1673 details = "full" 1674 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1675 1676 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1677 1678 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1679 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1680 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1681 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1682 1683 # save response headers without "positions" section: 1684 for key in portfolioResponse.keys(): 1685 if key != "positions": 1686 view["raw"]["headers"][key] = portfolioResponse[key] 1687 1688 else: 1689 continue 1690 1691 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1692 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1693 for item in portfolioResponse["positions"]: 1694 if item["instrumentType"] == "currency": 1695 self.figi = item["figi"] 1696 curr = self.SearchByFIGI(requestPrice=False) 1697 1698 # current price of currency in RUB: 1699 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1700 "name": curr["name"], 1701 "currentPrice": NanoToFloat( 1702 item["currentPrice"]["units"], 1703 item["currentPrice"]["nano"] 1704 ), 1705 } 1706 1707 view["raw"]["Currencies"].append(item) 1708 1709 elif item["instrumentType"] == "share": 1710 view["raw"]["Shares"].append(item) 1711 1712 elif item["instrumentType"] == "bond": 1713 view["raw"]["Bonds"].append(item) 1714 1715 elif item["instrumentType"] == "etf": 1716 view["raw"]["Etfs"].append(item) 1717 1718 elif item["instrumentType"] == "futures": 1719 view["raw"]["Futures"].append(item) 1720 1721 else: 1722 continue 1723 1724 # how many volume of currencies (by ISO currency name) are blocked: 1725 for item in view["raw"]["positions"]["blocked"]: 1726 blocked = NanoToFloat(item["units"], item["nano"]) 1727 if blocked > 0: 1728 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1729 1730 # how many volume of instruments (by FIGI) are blocked: 1731 for item in view["raw"]["positions"]["securities"]: 1732 blocked = int(item["blocked"]) 1733 if blocked > 0: 1734 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1735 1736 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1737 1738 if "rub" in allBlocked.keys(): 1739 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1740 1741 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1742 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1743 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1744 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1745 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1746 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1747 view["stat"]["portfolioCostRUB"] = sum([ 1748 view["stat"]["allCurrenciesCostRUB"], 1749 view["stat"]["sharesCostRUB"], 1750 view["stat"]["bondsCostRUB"], 1751 view["stat"]["etfsCostRUB"], 1752 view["stat"]["futuresCostRUB"], 1753 ]) 1754 1755 # --- calculating some portfolio statistics: 1756 byComp = {} # distribution by companies 1757 bySect = {} # distribution by sectors 1758 byCurr = {} # distribution by currencies (include RUB) 1759 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1760 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1761 1762 for item in portfolioResponse["positions"]: 1763 self.figi = item["figi"] 1764 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1765 1766 if instrument: 1767 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1768 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1769 1770 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1771 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1772 1773 else: 1774 blocked = 0 1775 1776 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1777 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1778 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1779 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1780 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1781 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1782 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1783 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1784 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1785 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1786 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1787 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1788 1789 statData = { 1790 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1791 "ticker": instrument["ticker"], # ticker by FIGI 1792 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1793 "volume": volume, # available volume of instrument 1794 "lots": lots, # volume in lots of instrument 1795 "direction": direction, # direction of an instrument's position: short or long 1796 "blocked": blocked, # blocked volume of currency or instrument 1797 "currentPrice": curPrice, # current instrument's price in basic asset 1798 "average": average, # current average position price 1799 "cost": cost, # current cost of all volume of instrument in basic asset 1800 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1801 "costRUB": costRUB, # cost of instrument in ruble 1802 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1803 "profit": profit, # expected profit at current moment 1804 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1805 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1806 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1807 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1808 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1809 "step": instrument["step"], # minimum price increment 1810 } 1811 1812 # adding distribution by unique countries: 1813 if statData["country"] not in byCountry.keys(): 1814 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1815 1816 else: 1817 byCountry[statData["country"]]["cost"] += costRUB 1818 byCountry[statData["country"]]["percent"] += percentCostRUB 1819 1820 if item["instrumentType"] != "currency": 1821 # adding distribution by unique companies: 1822 if statData["name"]: 1823 if statData["name"] not in byComp.keys(): 1824 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1825 1826 else: 1827 byComp[statData["name"]]["cost"] += costRUB 1828 byComp[statData["name"]]["percent"] += percentCostRUB 1829 1830 # adding distribution by unique sectors: 1831 if statData["sector"] not in bySect.keys(): 1832 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1833 1834 else: 1835 bySect[statData["sector"]]["cost"] += costRUB 1836 bySect[statData["sector"]]["percent"] += percentCostRUB 1837 1838 # adding distribution by unique currencies: 1839 if currency not in byCurr.keys(): 1840 byCurr[currency] = { 1841 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1842 "cost": costRUB, 1843 "percent": percentCostRUB 1844 } 1845 1846 else: 1847 byCurr[currency]["cost"] += costRUB 1848 byCurr[currency]["percent"] += percentCostRUB 1849 1850 # saving statistics for every instrument: 1851 if item["instrumentType"] == "currency": 1852 view["stat"]["Currencies"].append(statData) 1853 1854 # update dict with free funds for trading (total - blocked) by currencies 1855 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1856 view["stat"]["funds"][currency] = { 1857 "total": volume, 1858 "totalCostRUB": costRUB, # total volume cost in rubles 1859 "free": volume - blocked, 1860 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1861 } 1862 1863 elif item["instrumentType"] == "share": 1864 view["stat"]["Shares"].append(statData) 1865 1866 elif item["instrumentType"] == "bond": 1867 view["stat"]["Bonds"].append(statData) 1868 1869 elif item["instrumentType"] == "etf": 1870 view["stat"]["Etfs"].append(statData) 1871 1872 elif item["instrumentType"] == "Futures": 1873 view["stat"]["Futures"].append(statData) 1874 1875 else: 1876 continue 1877 1878 # total changes in Russian Ruble: 1879 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1880 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1881 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1882 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1883 view["stat"]["funds"]["rub"] = { 1884 "total": view["stat"]["availableRUB"], 1885 "totalCostRUB": view["stat"]["availableRUB"], 1886 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1887 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1888 } 1889 1890 # --- pending orders sector data: 1891 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1892 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1893 1894 for item in view["raw"]["orders"]: 1895 self.figi = item["figi"] 1896 1897 if item["figi"] not in uniquePendingOrdersFIGIs: 1898 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1899 1900 uniquePendingOrdersFIGIs.append(item["figi"]) 1901 uniquePendingOrders[item["figi"]] = instrument 1902 1903 else: 1904 instrument = uniquePendingOrders[item["figi"]] 1905 1906 if instrument: 1907 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1908 orderType = TKS_ORDER_TYPES[item["orderType"]] 1909 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1910 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1911 1912 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1913 if item["direction"] == "ORDER_DIRECTION_BUY": 1914 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1915 1916 else: 1917 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1918 1919 # requested price for order execution: 1920 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1921 1922 # necessary changes in percent to reach target from current price: 1923 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1924 1925 view["stat"]["orders"].append({ 1926 "orderID": item["orderId"], # orderId number parameter of current order 1927 "figi": item["figi"], # FIGI identification 1928 "ticker": instrument["ticker"], # ticker name by FIGI 1929 "lotsRequested": item["lotsRequested"], # requested lots value 1930 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1931 "currentPrice": lastPrice, # current instrument's price for defined action 1932 "targetPrice": target, # requested price for order execution in base currency 1933 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1934 "percentChanges": changes, # changes in percent to target from current price 1935 "currency": item["currency"], # instrument's currency name 1936 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1937 "type": orderType, # type of order from TKS_ORDER_TYPES 1938 "status": orderState, # order status from TKS_ORDER_STATES 1939 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1940 }) 1941 1942 # --- stop orders sector data: 1943 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1944 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1945 1946 for item in view["raw"]["stopOrders"]: 1947 self.figi = item["figi"] 1948 1949 if item["figi"] not in uniqueStopOrdersFIGIs: 1950 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1951 1952 uniqueStopOrdersFIGIs.append(item["figi"]) 1953 uniqueStopOrders[item["figi"]] = instrument 1954 1955 else: 1956 instrument = uniqueStopOrders[item["figi"]] 1957 1958 if instrument: 1959 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1960 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1961 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1962 1963 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1964 if "expirationTime" in item.keys(): 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1966 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1967 1968 else: 1969 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1970 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1971 1972 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1973 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1974 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1975 1976 else: 1977 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1978 1979 # requested price when stop-order executed: 1980 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1981 1982 # price for limit-order, set up when stop-order executed: 1983 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1984 1985 # necessary changes in percent to reach target from current price: 1986 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1987 1988 view["stat"]["stopOrders"].append({ 1989 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1990 "figi": item["figi"], # FIGI identification 1991 "ticker": instrument["ticker"], # ticker name by FIGI 1992 "lotsRequested": item["lotsRequested"], # requested lots value 1993 "currentPrice": lastPrice, # current instrument's price for defined action 1994 "targetPrice": target, # requested price for stop-order execution in base currency 1995 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1996 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1997 "percentChanges": changes, # changes in percent to target from current price 1998 "currency": item["currency"], # instrument's currency name 1999 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2000 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2001 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2002 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2003 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2004 }) 2005 2006 # --- calculating data for analytics section: 2007 # portfolio distribution by assets: 2008 view["analytics"]["distrByAssets"] = { 2009 "Ruble": { 2010 "uniques": 1, 2011 "cost": view["stat"]["availableRUB"], 2012 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2013 }, 2014 "Currencies": { 2015 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2016 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2017 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 }, 2019 "Shares": { 2020 "uniques": len(view["stat"]["Shares"]), 2021 "cost": view["stat"]["sharesCostRUB"], 2022 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2023 }, 2024 "Bonds": { 2025 "uniques": len(view["stat"]["Bonds"]), 2026 "cost": view["stat"]["bondsCostRUB"], 2027 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2028 }, 2029 "Etfs": { 2030 "uniques": len(view["stat"]["Etfs"]), 2031 "cost": view["stat"]["etfsCostRUB"], 2032 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 "Futures": { 2035 "uniques": len(view["stat"]["Futures"]), 2036 "cost": view["stat"]["futuresCostRUB"], 2037 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 } 2040 2041 # portfolio distribution by companies: 2042 view["analytics"]["distrByCompanies"]["All money cash"] = { 2043 "ticker": "", 2044 "cost": view["stat"]["allCurrenciesCostRUB"], 2045 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2046 } 2047 view["analytics"]["distrByCompanies"].update(byComp) 2048 2049 # portfolio distribution by sectors: 2050 view["analytics"]["distrBySectors"]["All money cash"] = { 2051 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2052 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2053 } 2054 view["analytics"]["distrBySectors"].update(bySect) 2055 2056 # portfolio distribution by currencies: 2057 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2058 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2059 2060 if self.moreDebug: 2061 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2062 2063 view["analytics"]["distrByCurrencies"].update(byCurr) 2064 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2065 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2066 2067 # portfolio distribution by countries: 2068 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2069 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2070 2071 if self.moreDebug: 2072 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2073 2074 view["analytics"]["distrByCountries"].update(byCountry) 2075 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2076 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2077 2078 # --- Prepare text statistics overview in human-readable: 2079 if show: 2080 # Whatever the value `details`, header not changes: 2081 info = [ 2082 "# Client's portfolio\n\n", 2083 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2084 "* **Account ID:** [{}]\n".format(self.accountId), 2085 ] 2086 2087 if details in ["full", "positions", "digest"]: 2088 info.extend([ 2089 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2090 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2091 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2092 view["stat"]["totalChangesRUB"], 2093 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2094 view["stat"]["totalChangesPercentRUB"], 2095 ), 2096 ]) 2097 2098 if details in ["full", "positions"]: 2099 info.extend([ 2100 "## Open positions\n\n", 2101 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2102 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2103 "| Ruble | {:>31} | | | | | |\n".format( 2104 "{:.2f} ({:.2f}) rub".format( 2105 view["stat"]["availableRUB"], 2106 view["stat"]["blockedRUB"], 2107 ) 2108 ) 2109 ]) 2110 2111 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2112 return [ 2113 "| | | | | | | |\n", 2114 "| {:<27} | | | | | {:>19} | |\n".format( 2115 noTradeStr if noTradeStr else typeStr, 2116 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2117 ), 2118 ] 2119 2120 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2121 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2122 "{} [{}]".format(data["ticker"], data["figi"]), 2123 "{:.2f} ({:.2f}) {}".format( 2124 data["volume"], 2125 data["blocked"], 2126 data["currency"], 2127 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2128 data["volume"], 2129 data["blocked"], 2130 ), 2131 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2132 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2133 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2134 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2135 "{}{:.2f} {} ({}{:.2f}%)".format( 2136 "+" if data["profit"] > 0 else "", 2137 data["profit"], data["baseCurrencyName"], 2138 "+" if data["percentProfit"] > 0 else "", 2139 data["percentProfit"], 2140 ), 2141 ) 2142 2143 # --- Show currencies section: 2144 if view["stat"]["Currencies"]: 2145 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2146 for item in view["stat"]["Currencies"]: 2147 info.append(_InfoStr(item, showCurrencyName=True)) 2148 2149 else: 2150 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2151 2152 # --- Show shares section: 2153 if view["stat"]["Shares"]: 2154 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2155 2156 for item in view["stat"]["Shares"]: 2157 info.append(_InfoStr(item)) 2158 2159 else: 2160 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2161 2162 # --- Show bonds section: 2163 if view["stat"]["Bonds"]: 2164 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2165 2166 for item in view["stat"]["Bonds"]: 2167 info.append(_InfoStr(item)) 2168 2169 else: 2170 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2171 2172 # --- Show etfs section: 2173 if view["stat"]["Etfs"]: 2174 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2175 2176 for item in view["stat"]["Etfs"]: 2177 info.append(_InfoStr(item)) 2178 2179 else: 2180 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2181 2182 # --- Show futures section: 2183 if view["stat"]["Futures"]: 2184 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2185 2186 for item in view["stat"]["Futures"]: 2187 info.append(_InfoStr(item)) 2188 2189 else: 2190 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2191 2192 if details in ["full", "orders"]: 2193 # --- Show pending orders section: 2194 if view["stat"]["orders"]: 2195 info.extend([ 2196 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2197 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2198 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2199 ]) 2200 2201 for item in view["stat"]["orders"]: 2202 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2203 "{} [{}]".format(item["ticker"], item["figi"]), 2204 item["orderID"], 2205 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2206 "{} {} ({}{:.2f}%)".format( 2207 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2208 item["baseCurrencyName"], 2209 "+" if item["percentChanges"] > 0 else "", 2210 float(item["percentChanges"]), 2211 ), 2212 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2213 item["action"], 2214 item["type"], 2215 item["date"], 2216 )) 2217 2218 else: 2219 info.append("\n## Total pending limit-orders: 0\n") 2220 2221 # --- Show stop orders section: 2222 if view["stat"]["stopOrders"]: 2223 info.extend([ 2224 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2225 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2226 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2227 ]) 2228 2229 for item in view["stat"]["stopOrders"]: 2230 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2231 "{} [{}]".format(item["ticker"], item["figi"]), 2232 item["orderID"], 2233 item["lotsRequested"], 2234 "{} {} ({}{:.2f}%)".format( 2235 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2236 item["baseCurrencyName"], 2237 "+" if item["percentChanges"] > 0 else "", 2238 float(item["percentChanges"]), 2239 ), 2240 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2241 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2242 item["action"], 2243 item["type"], 2244 item["expType"], 2245 item["createDate"], 2246 item["expDate"], 2247 )) 2248 2249 else: 2250 info.append("\n## Total stop-orders: 0\n") 2251 2252 if details in ["full", "analytics"]: 2253 # -- Show analytics section: 2254 if view["stat"]["portfolioCostRUB"] > 0: 2255 info.extend([ 2256 "\n# Analytics\n" 2257 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2258 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2259 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2260 view["stat"]["totalChangesRUB"], 2261 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2262 view["stat"]["totalChangesPercentRUB"], 2263 ), 2264 "\n## Portfolio distribution by assets\n" 2265 "\n| Type | Uniques | Percent | Current cost |\n", 2266 "|------------|---------|---------|--------------------|\n", 2267 ]) 2268 2269 for key in view["analytics"]["distrByAssets"].keys(): 2270 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2271 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2272 key, 2273 view["analytics"]["distrByAssets"][key]["uniques"], 2274 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2275 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2276 )) 2277 2278 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2279 info.extend([ 2280 "\n## Portfolio distribution by companies\n" 2281 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2282 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2283 ]) 2284 2285 for company in view["analytics"]["distrByCompanies"].keys(): 2286 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2287 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2288 info.append("| {} | {:<7} | {:<18} |\n".format( 2289 "{}{}{}".format( 2290 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2291 company, 2292 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2293 ), 2294 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2295 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2296 )) 2297 2298 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2299 info.extend([ 2300 "\n## Portfolio distribution by sectors\n" 2301 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2302 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2303 ]) 2304 2305 for sector in view["analytics"]["distrBySectors"].keys(): 2306 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2307 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2308 sector, 2309 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2310 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2311 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2312 )) 2313 2314 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2315 info.extend([ 2316 "\n## Portfolio distribution by currencies\n" 2317 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2318 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2319 ]) 2320 2321 for curr in view["analytics"]["distrByCurrencies"].keys(): 2322 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2323 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2324 info.append("| {} | {:<7} | {:<18} |\n".format( 2325 "[{}] {}{}".format( 2326 curr, 2327 view["analytics"]["distrByCurrencies"][curr]["name"], 2328 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2329 ), 2330 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2331 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2332 )) 2333 2334 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2335 info.extend([ 2336 "\n## Portfolio distribution by countries\n" 2337 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2338 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2339 ]) 2340 2341 for country in view["analytics"]["distrByCountries"].keys(): 2342 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2343 nameLen = len(country) 2344 info.append("| {} | {:<7} | {:<18} |\n".format( 2345 "{}{}".format( 2346 country, 2347 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2348 ), 2349 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2350 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2351 )) 2352 2353 infoText = "".join(info) 2354 2355 uLogger.info(infoText) 2356 2357 if details == "full" and self.overviewFile: 2358 filename = self.overviewFile 2359 2360 elif details == "digest" and self.overviewDigestFile: 2361 filename = self.overviewDigestFile 2362 2363 elif details == "positions" and self.overviewPositionsFile: 2364 filename = self.overviewPositionsFile 2365 2366 elif details == "orders" and self.overviewOrdersFile: 2367 filename = self.overviewOrdersFile 2368 2369 elif details == "analytics" and self.overviewAnalyticsFile: 2370 filename = self.overviewAnalyticsFile 2371 2372 else: 2373 filename = "" 2374 2375 if filename: 2376 with open(filename, "w", encoding="UTF-8") as fH: 2377 fH.write(infoText) 2378 2379 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2380 2381 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2383 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2384 """ 2385 Returns history operations between two given dates for current `accountId`. 2386 If `reportFile` string is not empty then also save human-readable report. 2387 Shows some statistical data of closed positions. 2388 2389 :param start: see docstring in `GetDatesAsString()` method 2390 :param end: see docstring in `GetDatesAsString()` method 2391 :param show: if `True` then also prints all records to the console. 2392 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2393 :return: original list of dictionaries with history of deals records from API ("operations" key): 2394 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2395 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2396 """ 2397 if self.accountId is None or not self.accountId: 2398 uLogger.error("Variable `accountId` must be defined for using this method!") 2399 raise Exception("Account ID required") 2400 2401 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2402 2403 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2404 2405 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2406 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2407 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2408 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2409 customStat = {} # custom statistics in additional to responseJSON 2410 2411 # --- output report in human-readable format: 2412 if show or self.reportFile: 2413 splitLine1 = "| | | | | |\n" # Summary section 2414 splitLine2 = "| | | | | | | | |\n" # Operations section 2415 nextDay = "" 2416 2417 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2418 2419 if len(ops) > 0: 2420 customStat = { 2421 "opsCount": 0, # total operations count 2422 "buyCount": 0, # buy operations 2423 "sellCount": 0, # sell operations 2424 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2425 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2426 "payIn": {"rub": 0.}, # Deposit brokerage account 2427 "payOut": {"rub": 0.}, # Withdrawals 2428 "divs": {"rub": 0.}, # Dividends income 2429 "coupons": {"rub": 0.}, # Coupon's income 2430 "brokerCom": {"rub": 0.}, # Service commissions 2431 "serviceCom": {"rub": 0.}, # Service commissions 2432 "marginCom": {"rub": 0.}, # Margin commissions 2433 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2434 } 2435 2436 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2437 for item in ops: 2438 if item["state"] == "OPERATION_STATE_EXECUTED": 2439 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2440 2441 # count buy operations: 2442 if "_BUY" in item["operationType"]: 2443 customStat["buyCount"] += 1 2444 2445 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2446 customStat["buyTotal"][item["payment"]["currency"]] += payment 2447 2448 else: 2449 customStat["buyTotal"][item["payment"]["currency"]] = payment 2450 2451 # count sell operations: 2452 elif "_SELL" in item["operationType"]: 2453 customStat["sellCount"] += 1 2454 2455 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2456 customStat["sellTotal"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["sellTotal"][item["payment"]["currency"]] = payment 2460 2461 # count incoming operations: 2462 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2463 if item["payment"]["currency"] in customStat["payIn"].keys(): 2464 customStat["payIn"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["payIn"][item["payment"]["currency"]] = payment 2468 2469 # count withdrawals operations: 2470 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2471 if item["payment"]["currency"] in customStat["payOut"].keys(): 2472 customStat["payOut"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["payOut"][item["payment"]["currency"]] = payment 2476 2477 # count dividends income: 2478 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2479 if item["payment"]["currency"] in customStat["divs"].keys(): 2480 customStat["divs"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["divs"][item["payment"]["currency"]] = payment 2484 2485 # count coupon's income: 2486 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2487 if item["payment"]["currency"] in customStat["coupons"].keys(): 2488 customStat["coupons"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["coupons"][item["payment"]["currency"]] = payment 2492 2493 # count broker commissions: 2494 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2495 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2496 customStat["brokerCom"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["brokerCom"][item["payment"]["currency"]] = payment 2500 2501 # count service commissions: 2502 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2503 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2504 customStat["serviceCom"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["serviceCom"][item["payment"]["currency"]] = payment 2508 2509 # count margin commissions: 2510 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2511 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2512 customStat["marginCom"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["marginCom"][item["payment"]["currency"]] = payment 2516 2517 # count withholding taxes: 2518 elif "_TAX" in item["operationType"]: 2519 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2520 customStat["allTaxes"][item["payment"]["currency"]] += payment 2521 2522 else: 2523 customStat["allTaxes"][item["payment"]["currency"]] = payment 2524 2525 else: 2526 continue 2527 2528 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2529 2530 # --- view "Actions" lines: 2531 info.extend([ 2532 "| Report sections | | | | |\n", 2533 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2534 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2535 "| | Buy: {:<22} | {:<28} | | |\n".format( 2536 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2537 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2538 ), 2539 "| | Sell: {:<21} | {:<28} | | |\n".format( 2540 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2541 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2542 ), 2543 ]) 2544 2545 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2546 for key in opsKeys: 2547 if key == "rub": 2548 continue 2549 2550 info.extend([ 2551 "| | | {:<28} | | |\n".format( 2552 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2553 ), 2554 "| | | {:<28} | | |\n".format( 2555 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2556 ), 2557 ]) 2558 2559 info.append(splitLine1) 2560 2561 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2562 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2563 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2564 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2565 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2566 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2567 ) 2568 2569 # --- view "Payments" lines: 2570 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2571 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2572 2573 for key in paymentsKeys: 2574 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2575 2576 info.append(splitLine1) 2577 2578 # --- view "Commissions and taxes" lines: 2579 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2580 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2581 2582 for key in comKeys: 2583 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2584 2585 info.append(splitLine1) 2586 2587 info.extend([ 2588 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2589 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2590 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2591 ]) 2592 2593 else: 2594 info.append("Broker returned no operations during this period\n") 2595 2596 # --- view "Operations" section: 2597 for item in ops: 2598 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2599 continue 2600 2601 else: 2602 self.figi = item["figi"] if item["figi"] else "" 2603 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2604 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2605 2606 # group of deals during one day: 2607 if nextDay and item["date"].split("T")[0] != nextDay: 2608 info.append(splitLine2) 2609 nextDay = "" 2610 2611 else: 2612 nextDay = item["date"].split("T")[0] # saving current day for splitting 2613 2614 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2615 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2616 self.figi if self.figi else "—", 2617 instrument["ticker"] if instrument else "—", 2618 instrument["type"] if instrument else "—", 2619 item["quantity"] if int(item["quantity"]) > 0 else "—", 2620 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2621 TKS_OPERATION_STATES[item["state"]], 2622 TKS_OPERATION_TYPES[item["operationType"]], 2623 )) 2624 2625 infoText = "".join(info) 2626 2627 if show: 2628 if self.moreDebug: 2629 uLogger.debug("Records about history of a client's operations successfully received") 2630 2631 uLogger.info(infoText) 2632 2633 if self.reportFile: 2634 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2635 fH.write(infoText) 2636 2637 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2638 2639 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2641 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2642 """ 2643 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2644 2645 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2646 Warning! Broker server used ISO UTC time by default. 2647 2648 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2649 Also, `historyFile` used to update history with `onlyMissing` parameter. 2650 2651 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2652 2653 :param start: see docstring in `GetDatesAsString()` method. 2654 :param end: see docstring in `GetDatesAsString()` method. 2655 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2656 `"hour"`, `"day"`. Default: `"hour"`. 2657 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2658 False by default. Warning! History appends only from last candle to current time 2659 with always update last candle! 2660 :param csvSep: separator if csv-file is used, `,` by default. 2661 :param show: if `True` then also prints Pandas DataFrame to the console. 2662 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2663 `["date", "time", "open", "high", "low", "close", "volume"]`. 2664 """ 2665 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2666 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2667 history = None # empty pandas object for history 2668 2669 if interval not in TKS_CANDLE_INTERVALS.keys(): 2670 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2671 raise Exception("Incorrect value") 2672 2673 if not (self.ticker or self.figi): 2674 uLogger.error("Ticker or FIGI must be defined!") 2675 raise Exception("Ticker or FIGI required") 2676 2677 if self.ticker and not self.figi: 2678 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2679 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2680 2681 if self.figi and not self.ticker: 2682 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2683 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2684 2685 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2686 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2687 if interval.lower() != "day": 2688 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2689 2690 delta = dtEnd - dtStart # current UTC time minus last time in file 2691 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2692 2693 # calculate history length in candles: 2694 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2695 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2696 length += 1 # to avoid fraction time 2697 2698 # calculate data blocks count: 2699 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2700 2701 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2702 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2703 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2704 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2705 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2706 2707 tempOld = None # pandas object for old history, if --only-missing key present 2708 lastTime = None # datetime object of last old candle in file 2709 2710 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2711 uLogger.debug("--only-missing key present, add only last missing candles...") 2712 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2713 2714 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2715 2716 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2717 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2718 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2719 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2720 2721 # get last datetime object from last string in file or minus 1 delta if file is empty: 2722 if len(tempOld) > 0: 2723 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2724 2725 else: 2726 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2727 2728 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2729 2730 responseJSONs = [] # raw history blocks of data 2731 2732 blockEnd = dtEnd 2733 for item in range(blocks): 2734 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2735 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2736 2737 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2738 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2739 )) 2740 2741 if blockStart == blockEnd: 2742 uLogger.debug("Skipped this zero-length block...") 2743 2744 else: 2745 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2746 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2747 self.body = str({ 2748 "figi": self.figi, 2749 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2750 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2751 "interval": TKS_CANDLE_INTERVALS[interval][0] 2752 }) 2753 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2754 2755 if "code" in responseJSON.keys(): 2756 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2757 2758 else: 2759 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2760 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2761 2762 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2763 2764 blockEnd = blockStart 2765 2766 printCount = len(responseJSONs) # candles to show in console 2767 if responseJSONs: 2768 tempHistory = pd.DataFrame( 2769 data={ 2770 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2771 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2772 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2773 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2774 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2775 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2776 "volume": [int(item["volume"]) for item in responseJSONs], 2777 }, 2778 index=range(len(responseJSONs)), 2779 columns=["date", "time", "open", "high", "low", "close", "volume"], 2780 ) 2781 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2782 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2783 2784 # append only newest candles to old history if --only-missing key present: 2785 if onlyMissing and tempOld is not None and lastTime is not None: 2786 index = 0 # find start index in tempHistory data: 2787 2788 for i, item in tempHistory.iterrows(): 2789 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2790 2791 if curTime == lastTime: 2792 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2793 index = i 2794 printCount = index + 1 2795 break 2796 2797 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2798 2799 else: 2800 history = tempHistory # if no `--only-missing` key then load full data from server 2801 2802 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2803 2804 if history is not None and not history.empty: 2805 if show: 2806 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2807 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2808 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2809 )) 2810 2811 else: 2812 uLogger.warning("Received an empty candles history!") 2813 2814 if self.historyFile is not None: 2815 if history is not None and not history.empty: 2816 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2817 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2818 2819 else: 2820 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2821 2822 else: 2823 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2824 2825 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2827 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2828 """ 2829 Load candles history from csv-file and return Pandas DataFrame object. 2830 2831 See also: `History()` and `ShowHistoryChart()` methods. 2832 2833 :param filePath: path to csv-file to open. 2834 """ 2835 loadedHistory = None # init candles data object 2836 2837 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2838 2839 if os.path.exists(filePath): 2840 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2841 2842 tfStr = self.priceModel.FormattedDelta( 2843 self.priceModel.timeframe, 2844 "{days} days {hours}h {minutes}m {seconds}s", 2845 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2846 self.priceModel.timeframe, 2847 "{hours}h {minutes}m {seconds}s", 2848 ) 2849 2850 if loadedHistory is not None and not loadedHistory.empty: 2851 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2852 len(loadedHistory), 2853 tfStr, 2854 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2855 ) 2856 2857 else: 2858 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2859 2860 else: 2861 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2862 2863 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2865 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2866 """ 2867 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2868 2869 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2870 Default: `index.html` (both for interact and non-interact candlesticks chart). 2871 2872 See also: `History()` and `LoadHistory()` methods. 2873 2874 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2875 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2876 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2877 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2878 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2879 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2880 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2881 """ 2882 if isinstance(candles, str): 2883 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2884 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2885 2886 elif isinstance(candles, pd.DataFrame): 2887 self.priceModel.prices = candles # set candles chain from variable 2888 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2889 2890 if "datetime" not in candles.columns: 2891 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2892 2893 else: 2894 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2895 raise Exception("Incorrect value") 2896 2897 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2898 2899 if interact: 2900 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2901 2902 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2903 2904 else: 2905 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2906 2907 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2908 2909 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2911 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2912 """ 2913 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2914 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2915 2916 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2917 2918 :param operation: string "Buy" or "Sell". 2919 :param lots: volume, integer count of lots >= 1. 2920 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2921 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2922 :param expDate: string "Undefined" by default or local date in future, 2923 it is a string with format `%Y-%m-%d %H:%M:%S`. 2924 :return: JSON with response from broker server. 2925 """ 2926 if self.accountId is None or not self.accountId: 2927 uLogger.error("Variable `accountId` must be defined for using this method!") 2928 raise Exception("Account ID required") 2929 2930 if operation is None or not operation or operation not in ("Buy", "Sell"): 2931 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2932 raise Exception("Incorrect value") 2933 2934 if lots is None or lots < 1: 2935 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2936 lots = 1 2937 2938 if tp is None or tp < 0: 2939 tp = 0 2940 2941 if sl is None or sl < 0: 2942 sl = 0 2943 2944 if expDate is None or not expDate: 2945 expDate = "Undefined" 2946 2947 if not (self.ticker or self.figi): 2948 uLogger.error("Ticker or FIGI must be defined!") 2949 raise Exception("Ticker or FIGI required") 2950 2951 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2952 self.ticker = instrument["ticker"] 2953 self.figi = instrument["figi"] 2954 2955 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2956 2957 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2958 self.body = str({ 2959 "figi": self.figi, 2960 "quantity": str(lots), 2961 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2962 "accountId": str(self.accountId), 2963 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2964 }) 2965 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2966 2967 if "orderId" in response.keys(): 2968 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2969 operation, response["orderId"], 2970 self.ticker, self.figi, lots, 2971 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2972 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2973 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2974 )) 2975 2976 if tp > 0: 2977 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2978 2979 if sl > 0: 2980 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2981 2982 else: 2983 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2984 2985 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2987 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2988 """ 2989 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2990 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2991 2992 See also: `Order()` and `Trade()` docstrings. 2993 2994 :param lots: volume, integer count of lots >= 1. 2995 :param tp: float > 0, take profit price of stop-order. 2996 :param sl: float > 0, stop loss price of stop-order. 2997 :param expDate: it's a local date in future. 2998 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2999 :return: JSON with response from broker server. 3000 """ 3001 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3003 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3004 """ 3005 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3006 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3007 3008 See also: `Order()` and `Trade()` docstrings. 3009 3010 :param lots: volume, integer count of lots >= 1. 3011 :param tp: float > 0, take profit price of stop-order. 3012 :param sl: float > 0, stop loss price of stop-order. 3013 :param expDate: it's a local date in the future. 3014 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3015 :return: JSON with response from broker server. 3016 """ 3017 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3019 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3020 """ 3021 Close position of given instruments. 3022 3023 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3024 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3025 This avoids unnecessary downloading data from the server. 3026 """ 3027 if instruments is None or not instruments: 3028 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3029 raise Exception("Ticker or FIGI required") 3030 3031 if isinstance(instruments, str): 3032 instruments = [instruments] 3033 3034 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3035 if uniqueInstruments: 3036 if portfolio is None or not portfolio: 3037 portfolio = self.Overview(show=False) 3038 3039 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3040 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3041 3042 for self.figi in uniqueInstruments: 3043 if self.figi not in allOpened: 3044 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3045 continue 3046 3047 # search open trade info about instrument by ticker: 3048 instrument = {} 3049 for iType in TKS_INSTRUMENTS: 3050 if instrument: 3051 break 3052 3053 for item in portfolio["stat"][iType]: 3054 if item["figi"] == self.figi: 3055 instrument = item 3056 break 3057 3058 if instrument: 3059 self.ticker = instrument["ticker"] 3060 self.figi = instrument["figi"] 3061 3062 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3063 self.ticker, 3064 self.figi, 3065 int(instrument["volume"]), 3066 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3067 )) 3068 3069 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3070 3071 if tradeLots > 0: 3072 if instrument["blocked"] > 0: 3073 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3074 instrument["blocked"], 3075 self.ticker, 3076 tradeLots, 3077 )) 3078 3079 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3080 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3081 3082 else: 3083 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3085 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3086 """ 3087 Close all positions of given instruments with defined type. 3088 3089 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3090 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3091 This avoids unnecessary downloading data from the server. 3092 """ 3093 if iType not in TKS_INSTRUMENTS: 3094 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3095 3096 else: 3097 if portfolio is None or not portfolio: 3098 portfolio = self.Overview(show=False) 3099 3100 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3101 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3102 3103 if tickers and portfolio: 3104 self.CloseTrades(tickers, portfolio) 3105 3106 else: 3107 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3109 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3110 """ 3111 Universal method to create market or limit orders with all available parameters for current `accountId`. 3112 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3113 3114 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3115 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3116 3117 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3118 then broker immediately open market order as you can do simple --buy or --sell operations! 3119 3120 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3121 When current price will go up or down to target price value then broker opens a limit order. 3122 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3123 3124 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3125 3126 :param operation: string "Buy" or "Sell". 3127 :param orderType: string "Limit" or "Stop". 3128 :param lots: volume, integer count of lots >= 1. 3129 :param targetPrice: target price > 0. This is open trade price for limit order. 3130 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3131 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3132 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3133 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3134 Stop loss order always executed by market price. 3135 :param expDate: string "Undefined" by default or local date in future. 3136 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3137 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3138 A limit order has no expiration date, it lasts until the end of the trading day. 3139 :return: JSON with response from broker server. 3140 """ 3141 if self.accountId is None or not self.accountId: 3142 uLogger.error("Variable `accountId` must be defined for using this method!") 3143 raise Exception("Account ID required") 3144 3145 if operation is None or not operation or operation not in ("Buy", "Sell"): 3146 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3147 raise Exception("Incorrect value") 3148 3149 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3150 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3151 raise Exception("Incorrect value") 3152 3153 if lots is None or lots < 1: 3154 uLogger.error("You must define trade volume > 0: integer count of lots!") 3155 raise Exception("Incorrect value") 3156 3157 if targetPrice is None or targetPrice <= 0: 3158 uLogger.error("Target price for limit-order must be greater than 0!") 3159 raise Exception("Incorrect value") 3160 3161 if limitPrice is None or limitPrice <= 0: 3162 limitPrice = targetPrice 3163 3164 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3165 stopType = "Limit" 3166 3167 if expDate is None or not expDate: 3168 expDate = "Undefined" 3169 3170 if not (self.ticker or self.figi): 3171 uLogger.error("Tocker or FIGI must be defined!") 3172 raise Exception("Ticker or FIGI required") 3173 3174 response = {} 3175 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3176 self.ticker = instrument["ticker"] 3177 self.figi = instrument["figi"] 3178 3179 if orderType == "Limit": 3180 uLogger.debug( 3181 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3182 self.ticker, self.figi, 3183 operation, lots, targetPrice, instrument["currency"], 3184 )) 3185 3186 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3187 self.body = str({ 3188 "figi": self.figi, 3189 "quantity": str(lots), 3190 "price": FloatToNano(targetPrice), 3191 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3192 "accountId": str(self.accountId), 3193 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3194 }) 3195 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3196 3197 if "orderId" in response.keys(): 3198 uLogger.info( 3199 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3200 response["orderId"], 3201 self.ticker, self.figi, 3202 operation, lots, targetPrice, instrument["currency"], 3203 )) 3204 3205 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3206 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3207 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3208 targetPrice, instrument["currency"], 3209 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3210 )) 3211 3212 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3213 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3214 targetPrice, instrument["currency"], 3215 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3216 )) 3217 3218 else: 3219 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3220 3221 if orderType == "Stop": 3222 uLogger.debug( 3223 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3224 self.ticker, self.figi, 3225 operation, lots, 3226 targetPrice, instrument["currency"], 3227 limitPrice, instrument["currency"], 3228 stopType, expDate, 3229 )) 3230 3231 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3232 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3233 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3234 3235 body = { 3236 "figi": self.figi, 3237 "quantity": str(lots), 3238 "price": FloatToNano(limitPrice), 3239 "stopPrice": FloatToNano(targetPrice), 3240 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3241 "accountId": str(self.accountId), 3242 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3243 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3244 } 3245 3246 if expDateUTC: 3247 body["expireDate"] = expDateUTC 3248 3249 self.body = str(body) 3250 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3251 3252 if "stopOrderId" in response.keys(): 3253 uLogger.info( 3254 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3255 response["stopOrderId"], 3256 self.ticker, self.figi, 3257 operation, lots, 3258 targetPrice, instrument["currency"], 3259 limitPrice, instrument["currency"], 3260 TKS_STOP_ORDER_TYPES[stopOrderType], 3261 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3262 )) 3263 3264 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3265 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3266 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3267 targetPrice, instrument["currency"], 3268 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3269 )) 3270 3271 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3272 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3273 targetPrice, instrument["currency"], 3274 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3275 )) 3276 3277 else: 3278 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3279 3280 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3282 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3283 """ 3284 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3285 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3286 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3287 See also: `Order()` docstring. 3288 3289 :param lots: volume, integer count of lots >= 1. 3290 :param targetPrice: target price > 0. This is open trade price for limit order. 3291 :return: JSON with response from broker server. 3292 """ 3293 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3295 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3296 """ 3297 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3298 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3299 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3300 target price value then broker opens a limit order. See also: `Order()` docstring. 3301 3302 :param lots: volume, integer count of lots >= 1. 3303 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3304 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3305 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3306 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3307 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3308 :param expDate: string "Undefined" by default or local date in future. 3309 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3310 This date is converting to UTC format for server. 3311 :return: JSON with response from broker server. 3312 """ 3313 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3315 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3316 """ 3317 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3318 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3319 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3320 See also: `Order()` docstring. 3321 3322 :param lots: volume, integer count of lots >= 1. 3323 :param targetPrice: target price > 0. This is open trade price for limit order. 3324 :return: JSON with response from broker server. 3325 """ 3326 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3328 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3329 """ 3330 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3331 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3332 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3333 target price value then broker opens a limit order. See also: `Order()` docstring. 3334 3335 :param lots: volume, integer count of lots >= 1. 3336 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3337 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3338 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3339 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3340 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3341 :param expDate: string "Undefined" by default or local date in future. 3342 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3343 This date is converting to UTC format for server. 3344 :return: JSON with response from broker server. 3345 """ 3346 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3348 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3349 """ 3350 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3351 3352 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3353 :param allOrdersIDs: pre-received lists of all active pending orders. 3354 This avoids unnecessary downloading data from the server. 3355 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3356 """ 3357 if self.accountId is None or not self.accountId: 3358 uLogger.error("Variable `accountId` must be defined for using this method!") 3359 raise Exception("Account ID required") 3360 3361 if orderIDs: 3362 if allOrdersIDs is None or not allOrdersIDs: 3363 rawOrders = self.RequestPendingOrders() 3364 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3365 3366 if allStopOrdersIDs is None or not allStopOrdersIDs: 3367 rawStopOrders = self.RequestStopOrders() 3368 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3369 3370 for orderID in orderIDs: 3371 idInPendingOrders = orderID in allOrdersIDs 3372 idInStopOrders = orderID in allStopOrdersIDs 3373 3374 if not (idInPendingOrders or idInStopOrders): 3375 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3376 continue 3377 3378 else: 3379 if idInPendingOrders: 3380 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3381 3382 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3383 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3384 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3385 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3386 3387 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3388 if self.moreDebug: 3389 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3390 3391 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3392 3393 else: 3394 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3395 3396 elif idInStopOrders: 3397 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3398 3399 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3400 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3401 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3402 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3403 3404 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3405 if self.moreDebug: 3406 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3407 3408 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3409 3410 else: 3411 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3412 3413 else: 3414 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3416 def CloseAllOrders(self) -> None: 3417 """ 3418 Gets a list of open pending and stop orders and cancel it all. 3419 """ 3420 rawOrders = self.RequestPendingOrders() 3421 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3422 lenOrders = len(allOrdersIDs) 3423 3424 rawStopOrders = self.RequestStopOrders() 3425 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3426 lenSOrders = len(allStopOrdersIDs) 3427 3428 if lenOrders > 0 or lenSOrders > 0: 3429 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3430 3431 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3432 3433 else: 3434 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3436 def CloseAll(self, *args) -> None: 3437 """ 3438 Close all available (not blocked) opened trades and orders. 3439 3440 Also, you can select one or more keywords case-insensitive: 3441 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3442 3443 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3444 """ 3445 overview = self.Overview(show=False) # get all open trades info 3446 3447 if len(args) == 0: 3448 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3449 self.CloseAllOrders() # close all pending and stop orders 3450 3451 for iType in TKS_INSTRUMENTS: 3452 if iType != "Currencies": 3453 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3454 3455 else: 3456 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3457 lowerArgs = [x.lower() for x in args] 3458 3459 if "orders" in lowerArgs: 3460 self.CloseAllOrders() # close all pending and stop orders 3461 3462 for iType in TKS_INSTRUMENTS: 3463 if iType.lower() in lowerArgs and iType != "Currencies": 3464 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3466 @staticmethod 3467 def ParseOrderParameters(operation, **inputParameters): 3468 """ 3469 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3470 3471 :param operation: string "Buy" or "Sell". 3472 :param inputParameters: this is dict of strings that looks like this 3473 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3474 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3475 "prices" key: one or more prices to open limit-orders 3476 Counts of values in lots and prices lists must be equals! 3477 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3478 """ 3479 # TODO: update order grid work with api v2 3480 pass 3481 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3482 # 3483 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3484 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3485 # raise Exception("Incorrect value") 3486 # 3487 # if "l" in inputParameters.keys(): 3488 # inputParameters["lots"] = inputParameters.pop("l") 3489 # 3490 # if "p" in inputParameters.keys(): 3491 # inputParameters["prices"] = inputParameters.pop("p") 3492 # 3493 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3494 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3495 # raise Exception("Incorrect value") 3496 # 3497 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3498 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3499 # 3500 # if len(lots) != len(prices): 3501 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3502 # raise Exception("Incorrect value") 3503 # 3504 # uLogger.debug("Extracted parameters for orders:") 3505 # uLogger.debug("lots = {}".format(lots)) 3506 # uLogger.debug("prices = {}".format(prices)) 3507 # 3508 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3509 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3510 # uLogger.debug("Order parameters: {}".format(result)) 3511 # 3512 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3514 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3515 """ 3516 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3517 3518 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3519 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3520 """ 3521 result = False 3522 msg = "Instrument not defined!" 3523 3524 if portfolio is None or not portfolio: 3525 portfolio = self.Overview(show=False) 3526 3527 if self.ticker: 3528 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3529 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3530 3531 for iType in TKS_INSTRUMENTS: 3532 for instrument in portfolio["stat"][iType]: 3533 if instrument["ticker"] == self.ticker: 3534 result = True 3535 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3536 break 3537 3538 elif self.figi: 3539 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3540 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3541 3542 for iType in TKS_INSTRUMENTS: 3543 for instrument in portfolio["stat"][iType]: 3544 if instrument["figi"] == self.figi: 3545 result = True 3546 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3547 break 3548 3549 else: 3550 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3551 3552 uLogger.debug(msg) 3553 3554 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3556 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3557 """ 3558 Returns instrument is in the user's portfolio if it presents there. 3559 Instrument must be defined by `ticker` (highly priority) or `figi`. 3560 3561 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3562 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3563 """ 3564 result = None 3565 msg = "Instrument not defined!" 3566 3567 if portfolio is None or not portfolio: 3568 portfolio = self.Overview(show=False) 3569 3570 if self.ticker: 3571 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3572 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3573 3574 for iType in TKS_INSTRUMENTS: 3575 for instrument in portfolio["stat"][iType]: 3576 if instrument["ticker"] == self.ticker: 3577 result = instrument 3578 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3579 break 3580 3581 elif self.figi: 3582 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3583 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3584 3585 for iType in TKS_INSTRUMENTS: 3586 for instrument in portfolio["stat"][iType]: 3587 if instrument["figi"] == self.figi: 3588 result = instrument 3589 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3590 break 3591 3592 else: 3593 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3594 3595 uLogger.debug(msg) 3596 3597 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3599 def RequestLimits(self) -> dict: 3600 """ 3601 Method for obtaining the available funds for withdrawal for current `accountId`. 3602 3603 See also: 3604 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3605 - `OverviewLimits()` method 3606 3607 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3608 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3609 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3610 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3611 """ 3612 if self.accountId is None or not self.accountId: 3613 uLogger.error("Variable `accountId` must be defined for using this method!") 3614 raise Exception("Account ID required") 3615 3616 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3617 3618 self.body = str({"accountId": self.accountId}) 3619 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3620 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3621 3622 if self.moreDebug: 3623 uLogger.debug("Records about available funds for withdrawal successfully received") 3624 3625 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3627 def OverviewLimits(self, show: bool = False) -> dict: 3628 """ 3629 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3630 3631 See also: `RequestLimits()`. 3632 3633 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3634 :return: dict with raw parsed data from server and some calculated statistics about it. 3635 """ 3636 if self.accountId is None or not self.accountId: 3637 uLogger.error("Variable `accountId` must be defined for using this method!") 3638 raise Exception("Account ID required") 3639 3640 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3641 3642 view = { 3643 "rawLimits": rawLimits, 3644 "limits": { # parsed data for every currency: 3645 "money": { # this is an array of portfolio currency positions 3646 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3647 }, 3648 "blocked": { # this is an array of blocked currency 3649 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3650 }, 3651 "blockedGuarantee": { # this is locked money under collateral for futures 3652 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3653 }, 3654 }, 3655 } 3656 3657 # --- Prepare text table with limits in human-readable format: 3658 if show: 3659 info = [ 3660 "# Withdrawal limits\n\n", 3661 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3662 "* **Account ID:** [{}]\n".format(self.accountId), 3663 ] 3664 3665 if view["limits"]["money"]: 3666 info.extend([ 3667 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3668 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3669 ]) 3670 3671 else: 3672 info.append("\nNo withdrawal limits\n") 3673 3674 for curr in view["limits"]["money"].keys(): 3675 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3676 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3677 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3678 3679 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3680 "[{}]".format(curr), 3681 "{:.2f}".format(view["limits"]["money"][curr]), 3682 "{:.2f}".format(availableMoney), 3683 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3684 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3685 ) 3686 3687 if curr == "rub": 3688 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3689 3690 else: 3691 info.append(infoStr) 3692 3693 infoText = "".join(info) 3694 3695 uLogger.info(infoText) 3696 3697 if self.withdrawalLimitsFile: 3698 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3699 fH.write(infoText) 3700 3701 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3702 3703 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3705 def RequestAccounts(self) -> dict: 3706 """ 3707 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3708 3709 See also: 3710 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3711 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3712 - `OverviewUserInfo()` method 3713 3714 :return: dict with raw data from server that contains accounts info. Example of dict: 3715 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3716 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3717 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3718 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3719 """ 3720 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3721 3722 self.body = str({}) 3723 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3724 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3725 3726 if self.moreDebug: 3727 uLogger.debug("Records about available accounts successfully received") 3728 3729 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3731 def RequestUserInfo(self) -> dict: 3732 """ 3733 Method for requesting common user's information. 3734 3735 See also: 3736 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3737 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3738 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3739 - `OverviewUserInfo()` method 3740 3741 :return: dict with raw data from server that contains user's information. Example of dict: 3742 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3743 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3744 """ 3745 uLogger.debug("Requesting common user's information. Wait, please...") 3746 3747 self.body = str({}) 3748 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3749 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3750 3751 if self.moreDebug: 3752 uLogger.debug("Records about current user successfully received") 3753 3754 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3756 def RequestMarginStatus(self, accountId: str = None) -> dict: 3757 """ 3758 Method for requesting margin calculation for defined account ID. 3759 3760 See also: 3761 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3762 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3763 - `OverviewUserInfo()` method 3764 3765 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3766 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3767 Example of responses: 3768 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3769 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3770 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3771 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3772 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3773 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3774 """ 3775 if accountId is None or not accountId: 3776 if self.accountId is None or not self.accountId: 3777 uLogger.error("Variable `accountId` must be defined for using this method!") 3778 raise Exception("Account ID required") 3779 3780 else: 3781 accountId = self.accountId # use `self.accountId` (main ID) by default 3782 3783 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3784 3785 self.body = str({"accountId": accountId}) 3786 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3787 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3788 3789 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3790 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3791 rawMargin = {} 3792 3793 else: 3794 if self.moreDebug: 3795 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3796 3797 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3799 def RequestTariffLimits(self) -> dict: 3800 """ 3801 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3802 3803 See also: 3804 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3805 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3806 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3807 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3808 - `OverviewUserInfo()` method 3809 3810 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3811 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3812 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3813 """ 3814 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3815 3816 self.body = str({}) 3817 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3818 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3819 3820 if self.moreDebug: 3821 uLogger.debug("Records with limits of current tariff successfully received") 3822 3823 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3825 def RequestBondCoupons(self, iJSON: dict) -> dict: 3826 """ 3827 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3828 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3829 All dates are in UTC timezone. 3830 3831 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3832 Documentation: 3833 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3834 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3835 3836 See also: `ExtendBondsData()`. 3837 3838 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3839 If raw iJSON is not data of bond then server returns an error [400] with message: 3840 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3841 :return: dictionary with bond payment calendar. Response example 3842 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3843 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3844 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3845 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3846 """ 3847 if iJSON["figi"] is None or not iJSON["figi"]: 3848 uLogger.error("FIGI must be defined for using this method!") 3849 raise Exception("FIGI required") 3850 3851 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3852 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3853 3854 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3855 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3856 self.figi, 3857 startDate, 3858 endDate, 3859 )) 3860 3861 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3862 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3863 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3864 3865 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3866 uLogger.warning("Instrument type is not bond!") 3867 3868 else: 3869 if self.moreDebug: 3870 uLogger.debug("Records about bond payment calendar successfully received") 3871 3872 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3874 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3875 """ 3876 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3877 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3878 coupon yields, current yields and some statistics etc. 3879 3880 WARNING! This is too long operation if a lot of bonds requested from broker server. 3881 3882 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3883 3884 :param instruments: list of strings with tickers or FIGIs. 3885 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3886 for further used by data scientists or stock analytics. 3887 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3888 In XLSX-file and Pandas DataFrame fields mean: 3889 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3890 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3891 """ 3892 if instruments is None or not instruments: 3893 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3894 raise Exception("Ticker or FIGI required") 3895 3896 if isinstance(instruments, str): 3897 instruments = [instruments] 3898 3899 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3900 3901 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3902 3903 iCount = len(uniqueInstruments) 3904 tooLong = iCount >= 20 3905 if tooLong: 3906 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3907 3908 bonds = None 3909 for i, self.figi in enumerate(uniqueInstruments): 3910 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3911 3912 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3913 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3914 rawBond = self.SearchByFIGI(requestPrice=True) 3915 3916 # Widen raw data with UTC current time (iData["actualDateTime"]): 3917 actualDate = datetime.now(tzutc()) 3918 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3919 3920 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3921 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3922 3923 # Replace some values with human-readable: 3924 iData["nominalCurrency"] = iData["nominal"]["currency"] 3925 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3926 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3927 iData["aciCurrency"] = iData["aciValue"]["currency"] 3928 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3929 iData["issueSize"] = int(iData["issueSize"]) 3930 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3931 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3932 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3933 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3934 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3935 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3936 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3937 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3938 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3939 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3940 3941 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3942 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3943 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3944 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3945 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3946 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3947 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3948 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3949 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3950 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3951 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3952 3953 # Widen raw data with calendar data from `rawCalendar` values: 3954 calendarData = [] 3955 if "events" in iData["rawCalendar"].keys(): 3956 for item in iData["rawCalendar"]["events"]: 3957 calendarData.append({ 3958 "couponDate": item["couponDate"], 3959 "couponNumber": int(item["couponNumber"]), 3960 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3961 "payCurrency": item["payOneBond"]["currency"], 3962 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3963 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3964 "couponStartDate": item["couponStartDate"], 3965 "couponEndDate": item["couponEndDate"], 3966 "couponPeriod": item["couponPeriod"], 3967 }) 3968 3969 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3970 if "maturityDate" not in iData.keys(): 3971 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3972 3973 # Widen raw data with Coupon Rate. 3974 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3975 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3976 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3977 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3978 3979 # Widen raw data with Yield to Maturity (YTM) on current date. 3980 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3981 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3982 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3983 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3984 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3985 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3986 3987 iData["calendar"] = calendarData # adds calendar at the end 3988 3989 # Remove not used data: 3990 iData.pop("uid") 3991 iData.pop("positionUid") 3992 iData.pop("currentPrice") 3993 iData.pop("rawCalendar") 3994 3995 colNames = list(iData.keys()) 3996 if bonds is None: 3997 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3998 3999 else: 4000 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4001 4002 else: 4003 uLogger.warning("Instrument is not a bond!") 4004 4005 processed = round(100 * (i + 1) / iCount, 1) 4006 if tooLong and processed % 5 == 0: 4007 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4008 4009 else: 4010 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4011 4012 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4013 4014 # Saving bonds from Pandas DataFrame to XLSX sheet: 4015 if xlsx and self.bondsXLSXFile: 4016 with pd.ExcelWriter( 4017 path=self.bondsXLSXFile, 4018 date_format=TKS_DATE_FORMAT, 4019 datetime_format=TKS_DATE_TIME_FORMAT, 4020 mode="w", 4021 ) as writer: 4022 bonds.to_excel( 4023 writer, 4024 sheet_name="Extended bonds data", 4025 index=True, 4026 encoding="UTF-8", 4027 freeze_panes=(1, 1), 4028 ) # saving as XLSX-file with freeze first row and column as headers 4029 4030 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4031 4032 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4034 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4035 """ 4036 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4037 4038 WARNING! This is too long operation if a lot of bonds requested from broker server. 4039 4040 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4041 4042 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4043 extended information about bonds: main info, current prices, bond payment calendar, 4044 coupon yields, current yields and some statistics etc. 4045 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4046 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4047 for further used by data scientists or stock analytics. 4048 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4049 """ 4050 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4051 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4052 4053 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4054 4055 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4056 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4057 calendar = None 4058 for bond in extBonds.iterrows(): 4059 for item in bond[1]["calendar"]: 4060 cData = { 4061 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4062 "couponDate": item["couponDate"], 4063 "figi": bond[1]["figi"], 4064 "ticker": bond[1]["ticker"], 4065 "name": bond[1]["name"], 4066 "couponNumber": item["couponNumber"], 4067 "payOneBond": item["payOneBond"], 4068 "payCurrency": item["payCurrency"], 4069 "couponType": item["couponType"], 4070 "couponPeriod": item["couponPeriod"], 4071 "fixDate": item["fixDate"], 4072 "couponStartDate": item["couponStartDate"], 4073 "couponEndDate": item["couponEndDate"], 4074 } 4075 4076 if calendar is None: 4077 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4078 4079 else: 4080 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4081 4082 if calendar is not None: 4083 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4084 4085 # Saving calendar from Pandas DataFrame to XLSX sheet: 4086 if xlsx: 4087 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4088 4089 with pd.ExcelWriter( 4090 path=xlsxCalendarFile, 4091 date_format=TKS_DATE_FORMAT, 4092 datetime_format=TKS_DATE_TIME_FORMAT, 4093 mode="w", 4094 ) as writer: 4095 humanReadable = calendar.copy(deep=True) 4096 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4097 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4098 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4099 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4100 humanReadable.columns = colNames # human-readable column names 4101 4102 humanReadable.to_excel( 4103 writer, 4104 sheet_name="Bond payments calendar", 4105 index=False, 4106 encoding="UTF-8", 4107 freeze_panes=(1, 2), 4108 ) # saving as XLSX-file with freeze first row and column as headers 4109 4110 del humanReadable # release df in memory 4111 4112 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4113 4114 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4116 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4117 """ 4118 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4119 Also, creates Markdown file with calendar data, `calendar.md` by default. 4120 4121 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4122 4123 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4124 extended information about bonds: main info, current prices, bond payment calendar, 4125 coupon yields, current yields and some statistics etc. 4126 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4127 :param show: if `True` then also printing bonds payment calendar to the console, 4128 otherwise save to file `calendarFile` only. `False` by default. 4129 :return: multilines text in Markdown format with bonds payment calendar as a table. 4130 """ 4131 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4132 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4133 4134 infoText = "# Bond payments calendar\n\n" 4135 4136 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4137 4138 if not (calendar is None or calendar.empty): 4139 splitLine = "| | | | | | | | | |\n" 4140 4141 info = [ 4142 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4143 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4144 ] 4145 4146 newMonth = False 4147 notOneBond = calendar["figi"].nunique() > 1 4148 for i, bond in enumerate(calendar.iterrows()): 4149 if newMonth and notOneBond: 4150 info.append(splitLine) 4151 4152 info.append( 4153 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4154 " √" if bond[1]["paid"] else " —", 4155 bond[1]["couponDate"].split("T")[0], 4156 bond[1]["figi"], 4157 bond[1]["ticker"], 4158 bond[1]["couponNumber"], 4159 "{} {}".format( 4160 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4161 bond[1]["payCurrency"], 4162 ), 4163 bond[1]["couponType"], 4164 bond[1]["couponPeriod"], 4165 bond[1]["fixDate"].split("T")[0], 4166 ) 4167 ) 4168 4169 if i < len(calendar.values) - 1: 4170 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4171 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4172 newMonth = False if curDate.month == nextDate.month else True 4173 4174 else: 4175 newMonth = False 4176 4177 infoText += "".join(info) 4178 4179 if show: 4180 uLogger.info("{}".format(infoText)) 4181 4182 if self.calendarFile is not None: 4183 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4184 fH.write(infoText) 4185 4186 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4187 4188 else: 4189 infoText += "No data\n" 4190 4191 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4193 def OverviewAccounts(self, show: bool = False) -> dict: 4194 """ 4195 Method for parsing and show simple table with all available user accounts. 4196 4197 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4198 4199 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4200 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4201 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4202 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4203 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4204 "closed": "—", "access": "Full access" }, ...}}` 4205 """ 4206 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4207 4208 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4209 accounts = { 4210 item["id"]: { 4211 "type": TKS_ACCOUNT_TYPES[item["type"]], 4212 "name": item["name"], 4213 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4214 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4215 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4216 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4217 } for item in rawAccounts["accounts"] 4218 } 4219 4220 # Raw and parsed data with some fields replaced in "stat" section: 4221 view = { 4222 "rawAccounts": rawAccounts, 4223 "stat": accounts, 4224 } 4225 4226 # --- Prepare simple text table with only accounts data in human-readable format: 4227 if show: 4228 info = [ 4229 "# User accounts\n\n", 4230 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4231 "| Account ID | Type | Status | Name |\n", 4232 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4233 ] 4234 4235 for account in view["stat"].keys(): 4236 info.extend([ 4237 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4238 account, 4239 view["stat"][account]["type"], 4240 view["stat"][account]["status"], 4241 view["stat"][account]["name"], 4242 ) 4243 ]) 4244 4245 infoText = "".join(info) 4246 4247 uLogger.info(infoText) 4248 4249 if self.userAccountsFile: 4250 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4251 fH.write(infoText) 4252 4253 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4254 4255 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4257 def OverviewUserInfo(self, show: bool = False) -> dict: 4258 """ 4259 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4260 4261 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4262 4263 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4264 :return: dict with raw parsed data from server and some calculated statistics about it. 4265 """ 4266 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4267 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4268 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4269 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4270 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4271 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4272 4273 # This is dict with parsed common user data: 4274 userInfo = { 4275 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4276 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4277 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4278 "tariff": rawUserInfo["tariff"], 4279 } 4280 4281 # This is an array of dict with parsed margin statuses for every account IDs: 4282 margins = {} 4283 for accountId in accounts.keys(): 4284 if rawMargins[accountId]: 4285 margins[accountId] = { 4286 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4287 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4288 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4289 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4290 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4291 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4292 } 4293 4294 else: 4295 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4296 4297 unary = {} # unary-connection limits 4298 for item in rawTariffLimits["unaryLimits"]: 4299 if item["limitPerMinute"] in unary.keys(): 4300 unary[item["limitPerMinute"]].extend(item["methods"]) 4301 4302 else: 4303 unary[item["limitPerMinute"]] = item["methods"] 4304 4305 stream = {} # stream-connection limits 4306 for item in rawTariffLimits["streamLimits"]: 4307 if item["limit"] in stream.keys(): 4308 stream[item["limit"]].extend(item["streams"]) 4309 4310 else: 4311 stream[item["limit"]] = item["streams"] 4312 4313 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4314 limits = { 4315 "unary": unary, 4316 "stream": stream, 4317 } 4318 4319 # Raw and parsed data as an output result: 4320 view = { 4321 "rawUserInfo": rawUserInfo, 4322 "rawAccounts": rawAccounts, 4323 "rawMargins": rawMargins, 4324 "rawTariffLimits": rawTariffLimits, 4325 "stat": { 4326 "userInfo": userInfo, 4327 "accounts": accounts, 4328 "margins": margins, 4329 "limits": limits, 4330 }, 4331 } 4332 4333 # --- Prepare text table with user information in human-readable format: 4334 if show: 4335 info = [ 4336 "# Full user information\n\n", 4337 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4338 "## Common information\n\n", 4339 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4340 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4341 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4342 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4343 "\n## User accounts\n\n", 4344 ] 4345 4346 for account in view["stat"]["accounts"].keys(): 4347 info.extend([ 4348 "### ID: [{}]\n\n".format(account), 4349 "| Parameters | Values |\n", 4350 "|----------------------|--------------------------------------------------------------|\n", 4351 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4352 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4353 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4354 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4355 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4356 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4357 ]) 4358 4359 if margins[account]: 4360 info.extend([ 4361 "| Margin status: | Enabled |\n", 4362 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4363 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4364 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4365 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4366 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4367 ]) 4368 4369 else: 4370 info.append("| Margin status: | Disabled |\n\n") 4371 4372 info.extend([ 4373 "\n## Current user tariff limits\n", 4374 "\nSee also:\n", 4375 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4376 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4377 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4378 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4379 "\n### Unary limits\n", 4380 ]) 4381 4382 if unary: 4383 for key, values in sorted(unary.items()): 4384 info.append("\n* Max requests per minute: {}\n".format(key)) 4385 4386 for value in values: 4387 info.append(" - {}\n".format(value)) 4388 4389 else: 4390 info.append("\nNot available\n") 4391 4392 info.append("\n### Stream limits\n") 4393 4394 if stream: 4395 for key, values in sorted(stream.items()): 4396 info.append("\n* Max stream connections: {}\n".format(key)) 4397 4398 for value in values: 4399 info.append(" - {}\n".format(value)) 4400 4401 else: 4402 info.append("\nNot available\n") 4403 4404 infoText = "".join(info) 4405 4406 uLogger.info(infoText) 4407 4408 if self.userInfoFile: 4409 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4410 fH.write(infoText) 4411 4412 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4413 4414 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4417class Args: 4418 """ 4419 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4420 """ 4421 def __init__(self, **kwargs): 4422 self.__dict__.update(kwargs) 4423 4424 def __getattr__(self, item): 4425 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4428def ParseArgs(): 4429 """This function get and parse command line keys.""" 4430 parser = ArgumentParser() # command-line string parser 4431 4432 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4433 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4434 4435 # --- options: 4436 4437 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4438 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4439 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4440 4441 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4442 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4443 4444 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4445 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4446 4447 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4448 4449 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4450 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4451 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4452 4453 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4454 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4455 4456 # --- commands: 4457 4458 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4459 4460 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4461 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4462 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4463 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4464 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4465 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4466 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4467 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4468 4469 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4470 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4471 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4472 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4473 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4474 4475 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4476 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4477 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4478 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4479 4480 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4481 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4482 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4483 4484 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4485 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4486 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4487 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4488 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4489 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4490 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4491 4492 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4493 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4494 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4495 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4496 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4497 4498 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4499 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4500 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4501 4502 cmdArgs = parser.parse_args() 4503 return cmdArgs
This function get and parse command line keys.
4506def Main(**kwargs): 4507 """ 4508 Main function for work with TKSBrokerAPI in the console. 4509 4510 See examples: 4511 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4512 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4513 """ 4514 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4515 4516 if args.debug_level: 4517 uLogger.level = 10 # always debug level by default 4518 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4519 4520 exitCode = 0 4521 start = datetime.now(tzutc()) 4522 uLogger.debug("=-" * 50) 4523 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4524 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4525 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4526 )) 4527 4528 # trying to calculate full current version: 4529 buildVersion = __version__ 4530 try: 4531 v = version("tksbrokerapi") 4532 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4533 4534 except Exception: 4535 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4536 4537 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4538 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4539 4540 try: 4541 if args.version: 4542 print("TKSBrokerAPI {}".format(buildVersion)) 4543 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4544 4545 else: 4546 # Init class for trading with Tinkoff Broker: 4547 trader = TinkoffBrokerServer( 4548 token=args.token, 4549 accountId=args.account_id, 4550 useCache=not args.no_cache, 4551 ) 4552 4553 # --- set some options: 4554 4555 if args.more: 4556 trader.moreDebug = True 4557 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4558 4559 if args.ticker: 4560 if args.ticker in trader.aliasesKeys: 4561 trader.ticker = trader.aliases[args.ticker] # Replace some tickers with its aliases 4562 4563 else: 4564 trader.ticker = args.ticker 4565 4566 if args.figi: 4567 trader.figi = args.figi 4568 4569 if args.depth is not None: 4570 trader.depth = args.depth 4571 4572 # --- do one command: 4573 4574 if args.list: 4575 if args.output is not None: 4576 trader.instrumentsFile = args.output 4577 4578 trader.ShowInstrumentsInfo(show=True) 4579 4580 elif args.list_xlsx: 4581 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4582 4583 elif args.bonds_xlsx is not None: 4584 if args.output is not None: 4585 trader.bondsXLSXFile = args.output 4586 4587 if len(args.bonds_xlsx) == 0: 4588 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4589 4590 else: 4591 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4592 4593 elif args.search: 4594 if args.output is not None: 4595 trader.searchResultsFile = args.output 4596 4597 trader.SearchInstruments(pattern=args.search[0], show=True) 4598 4599 elif args.info: 4600 if not (args.ticker or args.figi): 4601 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4602 raise Exception("Ticker or FIGI required") 4603 4604 if args.output is not None: 4605 trader.infoFile = args.output 4606 4607 if args.ticker: 4608 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4609 4610 else: 4611 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4612 4613 elif args.calendar is not None: 4614 if args.output is not None: 4615 trader.calendarFile = args.output 4616 4617 if len(args.calendar) == 0: 4618 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4619 4620 else: 4621 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4622 4623 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4624 4625 elif args.price: 4626 if not (args.ticker or args.figi): 4627 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4628 raise Exception("Ticker or FIGI required") 4629 4630 trader.GetCurrentPrices(show=True) 4631 4632 elif args.prices is not None: 4633 if args.output is not None: 4634 trader.pricesFile = args.output 4635 4636 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4637 4638 elif args.overview: 4639 if args.output is not None: 4640 trader.overviewFile = args.output 4641 4642 trader.Overview(show=True, details="full") 4643 4644 elif args.overview_digest: 4645 if args.output is not None: 4646 trader.overviewDigestFile = args.output 4647 4648 trader.Overview(show=True, details="digest") 4649 4650 elif args.overview_positions: 4651 if args.output is not None: 4652 trader.overviewPositionsFile = args.output 4653 4654 trader.Overview(show=True, details="positions") 4655 4656 elif args.overview_orders: 4657 if args.output is not None: 4658 trader.overviewOrdersFile = args.output 4659 4660 trader.Overview(show=True, details="orders") 4661 4662 elif args.overview_analytics: 4663 if args.output is not None: 4664 trader.overviewAnalyticsFile = args.output 4665 4666 trader.Overview(show=True, details="analytics") 4667 4668 elif args.deals is not None: 4669 if args.output is not None: 4670 trader.reportFile = args.output 4671 4672 if 0 <= len(args.deals) < 3: 4673 trader.Deals( 4674 start=args.deals[0] if len(args.deals) >= 1 else None, 4675 end=args.deals[1] if len(args.deals) == 2 else None, 4676 show=True, # Always show deals report in console 4677 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4678 ) 4679 4680 else: 4681 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4682 raise Exception("Incorrect value") 4683 4684 elif args.history is not None: 4685 if args.output is not None: 4686 trader.historyFile = args.output 4687 4688 if 0 <= len(args.history) < 3: 4689 dataReceived = trader.History( 4690 start=args.history[0] if len(args.history) >= 1 else None, 4691 end=args.history[1] if len(args.history) == 2 else None, 4692 interval="hour" if args.interval is None or not args.interval else args.interval, 4693 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4694 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4695 show=True, # shows all downloaded candles in console 4696 ) 4697 4698 if args.render_chart is not None and dataReceived is not None: 4699 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4700 4701 trader.ShowHistoryChart( 4702 candles=dataReceived, 4703 interact=iChart, 4704 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4705 ) 4706 4707 else: 4708 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4709 raise Exception("Incorrect value") 4710 4711 elif args.load_history is not None: 4712 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4713 4714 if args.render_chart is not None and histData is not None: 4715 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4716 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4717 4718 trader.ShowHistoryChart( 4719 candles=histData, 4720 interact=iChart, 4721 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4722 ) 4723 4724 elif args.trade is not None: 4725 if 1 <= len(args.trade) <= 5: 4726 trader.Trade( 4727 operation=args.trade[0], 4728 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4729 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4730 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4731 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4732 ) 4733 4734 else: 4735 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4736 4737 elif args.buy is not None: 4738 if 0 <= len(args.buy) <= 4: 4739 trader.Buy( 4740 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4741 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4742 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4743 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4744 ) 4745 4746 else: 4747 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4748 4749 elif args.sell is not None: 4750 if 0 <= len(args.sell) <= 4: 4751 trader.Sell( 4752 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4753 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4754 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4755 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4756 ) 4757 4758 else: 4759 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4760 4761 elif args.order: 4762 if 4 <= len(args.order) <= 7: 4763 trader.Order( 4764 operation=args.order[0], 4765 orderType=args.order[1], 4766 lots=int(args.order[2]), 4767 targetPrice=float(args.order[3]), 4768 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4769 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4770 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4771 ) 4772 4773 else: 4774 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4775 4776 elif args.buy_limit: 4777 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4778 4779 elif args.sell_limit: 4780 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4781 4782 elif args.buy_stop: 4783 if 2 <= len(args.buy_stop) <= 7: 4784 trader.BuyStop( 4785 lots=int(args.buy_stop[0]), 4786 targetPrice=float(args.buy_stop[1]), 4787 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4788 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4789 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4790 ) 4791 4792 else: 4793 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4794 4795 elif args.sell_stop: 4796 if 2 <= len(args.sell_stop) <= 7: 4797 trader.SellStop( 4798 lots=int(args.sell_stop[0]), 4799 targetPrice=float(args.sell_stop[1]), 4800 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4801 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4802 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4803 ) 4804 4805 else: 4806 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4807 4808 # elif args.buy_order_grid is not None: 4809 # # update order grid work with api v2 4810 # if len(args.buy_order_grid) == 2: 4811 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4812 # 4813 # for order in orderParams: 4814 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4815 # 4816 # else: 4817 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4818 # 4819 # elif args.sell_order_grid is not None: 4820 # # update order grid work with api v2 4821 # if len(args.sell_order_grid) >= 2: 4822 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4823 # 4824 # for order in orderParams: 4825 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4826 # 4827 # else: 4828 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4829 4830 elif args.close_order is not None: 4831 trader.CloseOrders(args.close_order) # close only one order 4832 4833 elif args.close_orders is not None: 4834 trader.CloseOrders(args.close_orders) # close list of orders 4835 4836 elif args.close_trade: 4837 if not (args.ticker or args.figi): 4838 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4839 raise Exception("Ticker or FIGI required") 4840 4841 if args.ticker: 4842 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4843 4844 else: 4845 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4846 4847 elif args.close_trades is not None: 4848 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4849 4850 elif args.close_all is not None: 4851 trader.CloseAll(*args.close_all) 4852 4853 elif args.limits: 4854 if args.output is not None: 4855 trader.withdrawalLimitsFile = args.output 4856 4857 trader.OverviewLimits(show=True) 4858 4859 elif args.user_info: 4860 if args.output is not None: 4861 trader.userInfoFile = args.output 4862 4863 trader.OverviewUserInfo(show=True) 4864 4865 elif args.account: 4866 if args.output is not None: 4867 trader.userAccountsFile = args.output 4868 4869 trader.OverviewAccounts(show=True) 4870 4871 else: 4872 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4873 raise Exception("There is no command to execute") 4874 4875 except Exception: 4876 trace = tb.format_exc() 4877 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4878 if e in trace: 4879 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4880 break 4881 4882 uLogger.debug(trace) 4883 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4884 exitCode = 255 # an error occurred, must be open a ticket for this issue 4885 4886 finally: 4887 finish = datetime.now(tzutc()) 4888 4889 if exitCode == 0: 4890 if args.more: 4891 uLogger.debug("All operations were finished success (summary code is 0).") 4892 4893 else: 4894 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4895 os.path.abspath(uLog.defaultLogFile), exitCode, 4896 )) 4897 4898 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4899 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4900 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4901 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4902 )) 4903 uLogger.debug("=-" * 50) 4904 4905 if not kwargs: 4906 sys.exit(exitCode) 4907 4908 else: 4909 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: